diff --git a/settings.gradle.kts b/settings.gradle.kts index 9bf43b5..be17148 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,8 @@ include("ts-web") include("ts-web:ts-web-netty") include("ts-web:ts-web-coroutine") include("ts-database") +include("ts-database:ts-mongodb") +include("ts-database:ts-mongodb:ts-mongodb-spring") //include("web", "aop", "database", "utils", "utils:xml", "utils:async-http", "web:netty-web") //include("socket", "socket:socket-async") //include("AsyncSocket") diff --git a/ts-core/ts-datastruct/src/main/kotlin/cn/tursom/core/datastruct/AsyncIterator.kt b/ts-core/ts-datastruct/src/main/kotlin/cn/tursom/core/datastruct/AsyncIterator.kt new file mode 100644 index 0000000..4a3414e --- /dev/null +++ b/ts-core/ts-datastruct/src/main/kotlin/cn/tursom/core/datastruct/AsyncIterator.kt @@ -0,0 +1,20 @@ +package cn.tursom.core.datastruct + +interface AsyncIterator { + /** + * Returns the next element in the iteration. + */ + suspend operator fun next(): T + + /** + * Returns `true` if the iteration has more elements. + */ + suspend operator fun hasNext(): Boolean +} + +suspend inline fun AsyncIterator.forEach(action: (T) -> Unit) { + while (hasNext()) { + val element = next() + action(element) + } +} \ No newline at end of file diff --git a/ts-database/src/main/kotlin/cn/tursom/database/AutoTable.kt b/ts-database/src/main/kotlin/cn/tursom/database/AutoTable.kt index b9e615d..617ce71 100644 --- a/ts-database/src/main/kotlin/cn/tursom/database/AutoTable.kt +++ b/ts-database/src/main/kotlin/cn/tursom/database/AutoTable.kt @@ -1,12 +1,10 @@ package cn.tursom.database +import cn.tursom.core.clone.Property +import cn.tursom.core.clone.inject +import cn.tursom.core.clone.instance +import cn.tursom.core.uncheckedCast import com.baomidou.mybatisplus.annotation.TableField -import com.ddbes.kotlin.clone.Property -import com.ddbes.kotlin.clone.inject -import com.ddbes.kotlin.clone.instance -import com.ddbes.kotlin.jdbc.simpTableField -import com.ddbes.kotlin.jdbc.tableName -import com.ddbes.kotlin.uncheckedCast import me.liuwj.ktorm.dsl.QueryRowSet import me.liuwj.ktorm.schema.BaseTable import me.liuwj.ktorm.schema.Column diff --git a/ts-database/ts-mongodb/build.gradle.kts b/ts-database/ts-mongodb/build.gradle.kts new file mode 100644 index 0000000..4fb8158 --- /dev/null +++ b/ts-database/ts-mongodb/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + kotlin("jvm") + `maven-publish` +} + +dependencies { + api(project(":")) + implementation(project(":ts-core")) + implementation(project(":ts-core:ts-datastruct")) + implementation(project(":ts-core:ts-log")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + api(group = "org.mongodb", name = "mongodb-driver-reactivestreams", version = "4.0.5") +} + +@kotlin.Suppress("UNCHECKED_CAST") +(rootProject.ext["excludeTest"] as (Project, TaskContainer) -> Unit)(project, tasks) + +tasks.register("install") { + finalizedBy(tasks["publishToMavenLocal"]) +} + +publishing { + publications { + create("maven") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + + from(components["java"]) + try { + artifact(tasks["sourcesJar"]) + } catch (e: Exception) { + } + } + } +} diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Aggregate.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Aggregate.kt new file mode 100644 index 0000000..318964d --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Aggregate.kt @@ -0,0 +1,111 @@ +package cn.tursom.database.mongodb + +import com.mongodb.MongoNamespace +import com.mongodb.client.model.* +import org.bson.conversions.Bson +import kotlin.reflect.KProperty1 + + +@Suppress("unused") +object Aggregate { + operator fun invoke(action: Aggregate.() -> Bson) = this.action() + + fun addFields(vararg fields: Field<*>): Bson = Aggregates.addFields(fields.asList()) + fun addFields(fields: List>): Bson? = Aggregates.addFields(fields) + + fun bucket( + groupBy: TExpression, + boundaries: List + ): Bson = Aggregates.bucket(groupBy, boundaries) + + fun bucket( + groupBy: TExpression, + boundaries: List, + options: BucketOptions + ): Bson = Aggregates.bucket(groupBy, boundaries, options) + + + fun bucketAuto(groupBy: TExpression, buckets: Int): Bson = Aggregates.bucketAuto(groupBy, buckets) + + fun bucketAuto( + groupBy: TExpression, + buckets: Int, + options: BucketAutoOptions + ): Bson = Aggregates.bucketAuto(groupBy, buckets, options) + + fun count(): Bson = Aggregates.count() + fun count(field: String): Bson = Aggregates.count(field) + + fun match(filter: Bson): Bson = Aggregates.match(filter) + fun match(value: Any): Bson = Aggregates.match(MongoUtil.convertToBson(value)) + fun project(projection: Bson): Bson = Aggregates.project(projection) + fun sort(sort: Bson): Bson = Aggregates.sort(sort) + fun sortByCount(filter: TExpression): Bson = Aggregates.sortByCount(filter) + fun skip(skip: Int): Bson = Aggregates.skip(skip) + fun limit(limit: Int): Bson = Aggregates.limit(limit) + + fun lookup( + from: String, + localField: String, + foreignField: String, + `as`: String + ): Bson = Aggregates.lookup(from, localField, foreignField, `as`) + + fun lookup(from: String, pipeline: List, `as`: String): Bson = Aggregates.lookup(from, pipeline, `as`) + fun lookup( + from: String, + let: List>? = null, + pipeline: List, + `as`: String + ): Bson = Aggregates.lookup(from, let, pipeline, `as`) + + + fun facet(facets: List): Bson = Aggregates.facet(facets) + fun facet(vararg facets: Facet): Bson = Aggregates.facet(facets.asList()) + + fun graphLookup( + from: String, + startWith: TExpression, + connectFromField: String, + connectToField: String, + `as`: String + ): Bson = Aggregates.graphLookup(from, startWith, connectFromField, connectToField, `as`) + + fun graphLookup( + from: String, + startWith: TExpression, + connectFromField: String, + connectToField: String, + `as`: String, + options: GraphLookupOptions + ): Bson = Aggregates.graphLookup(from, startWith, connectFromField, connectToField, `as`, options) + + + fun group( + id: TExpression? = null, + vararg fieldAccumulators: BsonField + ): Bson = Aggregates.group(id, fieldAccumulators.asList()) + + fun group( + id: TExpression? = null, + fieldAccumulators: List + ): Bson = Aggregates.group(id, fieldAccumulators) + + fun unwind(field: KProperty1<*, *>): Bson = Aggregates.unwind(MongoUtil.fieldName(field)) + fun unwind(field: KProperty1<*, *>, unwindOptions: UnwindOptions): Bson = + Aggregates.unwind(MongoUtil.fieldName(field), unwindOptions) + + fun unwind(fieldName: String): Bson = Aggregates.unwind(fieldName) + fun unwind(fieldName: String, unwindOptions: UnwindOptions): Bson = Aggregates.unwind(fieldName, unwindOptions) + + fun out(collectionName: String): Bson = Aggregates.out(collectionName) + + fun merge(collectionName: String): Bson = Aggregates.merge(collectionName) + fun merge(namespace: MongoNamespace): Bson = Aggregates.merge(namespace) + fun merge(collectionName: String, options: MergeOptions): Bson = Aggregates.merge(collectionName, options) + fun merge(namespace: MongoNamespace, options: MergeOptions): Bson = Aggregates.merge(namespace, options) + + fun replaceRoot(value: TExpression): Bson = Aggregates.replaceRoot(value) + fun replaceWith(value: TExpression): Bson = Aggregates.replaceWith(value) + fun sample(size: Int): Bson = Aggregates.sample(size) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactory.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactory.kt new file mode 100644 index 0000000..b6349c8 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactory.kt @@ -0,0 +1,28 @@ +package cn.tursom.database.mongodb + +import cn.tursom.core.Parser +import org.bson.Document + +interface BsonFactory { + val clazz: Class + + fun parse(document: Document) = Parser.parse(document, clazz)!! + + fun convertToBson(entity: T): Document + + //fun convertToEntity(bson: Document): T + + companion object { + operator fun get(clazz: Class): BsonFactory { + return BsonFactoryImpl(clazz) + } + + inline fun get(): BsonFactory { + return get(T::class.java) + } + + fun convertToBson(entity: Any): Document { + return get(entity.javaClass).convertToBson(entity) + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactoryImpl.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactoryImpl.kt new file mode 100644 index 0000000..1a2bf2b --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/BsonFactoryImpl.kt @@ -0,0 +1,25 @@ +package cn.tursom.database.mongodb + +import cn.tursom.core.isStatic +import cn.tursom.core.isTransient +import cn.tursom.database.mongodb.annotation.Ignore +import org.bson.Document + +open class BsonFactoryImpl(final override val clazz: Class) : BsonFactory { + private val fields = clazz.declaredFields.filter { + it.isAccessible = true + !it.isStatic() && !it.isTransient() && it.getAnnotation(Ignore::class.java) == null + } + + override fun convertToBson(entity: T): Document { + val bson = Document() + fields.forEach { + MongoUtil.injectValue(bson, it.get(entity) ?: return@forEach, it) + } + return bson + } + + override fun toString(): String { + return "BsonFactoryImpl(clazz=$clazz, fields=$fields)" + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/IndexBuilder.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/IndexBuilder.kt new file mode 100644 index 0000000..bc2a380 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/IndexBuilder.kt @@ -0,0 +1,136 @@ +package cn.tursom.database.mongodb + +import com.mongodb.client.model.Collation +import com.mongodb.client.model.IndexOptions +import org.bson.Document +import org.bson.conversions.Bson +import java.lang.reflect.Field +import java.util.concurrent.TimeUnit +import kotlin.reflect.KProperty + +@Suppress("unused") +class IndexBuilder { + val indexDocument = Document() + val indexOption = IndexOptions() + + var background + get() = indexOption.isBackground + set(value) { + indexOption.background(value) + } + var unique + get() = indexOption.isUnique + set(value) { + indexOption.unique(value) + } + var name: String? + get() = indexOption.name + set(value) { + indexOption.name(value) + } + var sparse + get() = indexOption.isSparse + set(value) { + indexOption.sparse(value) + } + var expireAfterSeconds: Long? + get() = indexOption.getExpireAfter(TimeUnit.SECONDS) + set(value) { + indexOption.expireAfter(value, TimeUnit.SECONDS) + } + + fun getExpireAfter(timeUnit: TimeUnit): Long? = indexOption.getExpireAfter(timeUnit) + fun expireAfter(expireAfter: Long?, timeUnit: TimeUnit): IndexOptions? = + indexOption.expireAfter(expireAfter, timeUnit) + + var version: Int? + get() = indexOption.version + set(value) { + indexOption.version(value) + } + var weights: Bson? + get() = indexOption.weights + set(value) { + indexOption.weights(value) + } + var defaultLanguage: String? + get() = indexOption.defaultLanguage + set(value) { + indexOption.defaultLanguage(value) + } + var languageOverride: String? + get() = indexOption.languageOverride + set(value) { + indexOption.languageOverride(value) + } + var textVersion: Int? + get() = indexOption.textVersion + set(value) { + indexOption.textVersion(value) + } + var sphereVersion: Int? + get() = indexOption.sphereVersion + set(value) { + indexOption.sphereVersion(value) + } + var bits: Int? + get() = indexOption.bits + set(value) { + indexOption.bits(value) + } + var min: Double? + get() = indexOption.min + set(value) { + indexOption.min(value) + } + var max: Double? + get() = indexOption.max + set(value) { + indexOption.max(value) + } + var bucketSize: Double? + get() = indexOption.bucketSize + set(value) { + indexOption.bucketSize(value) + } + var storageEngine: Bson? + get() = indexOption.storageEngine + set(value) { + indexOption.storageEngine(value) + } + var partialFilterExpression: Bson? + get() = indexOption.partialFilterExpression + set(value) { + indexOption.partialFilterExpression(value) + } + var collation: Collation? + get() = indexOption.collation + set(value) { + indexOption.collation(value) + } + var wildcardProjection: Bson + get() = indexOption.wildcardProjection + set(value) { + indexOption.wildcardProjection(value) + } + + + enum class SortEnum(val code: Int) { + DSC(1), DESC(-1) + } + + infix fun String.index(sort: SortEnum) { + indexDocument[this] = sort.code + } + + operator fun String.unaryPlus() = this index SortEnum.DSC + operator fun String.unaryMinus() = this index SortEnum.DESC + + infix fun KProperty<*>.index(sort: SortEnum) = MongoUtil.fieldName(this) index sort + operator fun KProperty<*>.unaryPlus() = +MongoUtil.fieldName(this) + operator fun KProperty<*>.unaryMinus() = -MongoUtil.fieldName(this) + + infix fun Field.index(sort: SortEnum) = MongoUtil.fieldName(this) index sort + operator fun Field.unaryPlus() = +MongoUtil.fieldName(this) + operator fun Field.unaryMinus() = -MongoUtil.fieldName(this) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoName.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoName.kt new file mode 100644 index 0000000..ad9edea --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoName.kt @@ -0,0 +1,27 @@ +package cn.tursom.database.mongodb + +import org.bson.Document +import java.lang.reflect.Field +import kotlin.reflect.KProperty + +interface MongoName { + val Class<*>.collectionName: String get() = MongoUtil.collectionName(this) + + val Field.fieldName: String get() = MongoUtil.fieldName(this) + + val KProperty<*>.fieldName get() = MongoUtil.fieldName(this) + + val Any.convertToBson: Document get() = MongoUtil.convertToBson(this) + + operator fun String.rem(field: String) = "$this.$field" + operator fun String.rem(field: KProperty<*>) = "$this.${field.fieldName}" + operator fun String.rem(field: Field) = "$this.${field.fieldName}" + + operator fun KProperty<*>.rem(field: String) = "$fieldName.$field" + operator fun KProperty<*>.rem(field: KProperty<*>) = "$fieldName.${field.fieldName}" + operator fun KProperty<*>.rem(field: Field) = "$fieldName.${field.fieldName}" + + operator fun Field.rem(field: String) = "$fieldName.$field" + operator fun Field.rem(field: KProperty<*>) = "$fieldName.${field.fieldName}" + operator fun Field.rem(field: Field) = "$fieldName.${field.fieldName}" +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoTemplate.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoTemplate.kt new file mode 100644 index 0000000..9b2e2a3 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoTemplate.kt @@ -0,0 +1,87 @@ +package cn.tursom.database.mongodb + +import cn.tursom.core.uncheckedCast +import cn.tursom.database.mongodb.operator.MongoOperator +import cn.tursom.database.mongodb.operator.MongoOperatorImpl +import com.mongodb.MongoClientSettings +import com.mongodb.ServerAddress +import com.mongodb.client.model.InsertManyOptions +import com.mongodb.client.model.InsertOneOptions +import com.mongodb.client.model.UpdateOptions +import com.mongodb.reactivestreams.client.MongoClient +import com.mongodb.reactivestreams.client.MongoClients +import com.mongodb.reactivestreams.client.MongoDatabase +import org.bson.conversions.Bson +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor + +class MongoTemplate( + private val db: MongoDatabase, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : MongoDatabase by db { + constructor( + mongoClient: MongoClient, + db: String, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, + ) : this(mongoClient.getDatabase(db), subscribeExecutor) + + constructor( + db: String, + mongoClientSettingsBuilder: MongoClientSettings.Builder, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, + ) : this(MongoClients.create(mongoClientSettingsBuilder.build()), db, subscribeExecutor) + + constructor( + db: String, + vararg servers: ServerAddress, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, + ) : this( + db, + MongoClientSettings.builder().also { + it.applyToClusterSettings { builder -> + builder.hosts(servers.asList()) + } + }, + subscribeExecutor + ) + + constructor(host: String, port: Int, db: String) : this(db, ServerAddress(host, port)) + + private val operatorMap = ConcurrentHashMap, MongoOperatorImpl<*>>() + + suspend fun save(entity: T, options: InsertOneOptions = InsertOneOptions()) = + getCollection(entity.javaClass).save(entity, options) + + suspend inline fun save(entities: Collection, options: InsertManyOptions = InsertManyOptions()) = + getCollection().save(entities, options) + + suspend fun update(entity: T, where: Bson, options: UpdateOptions = UpdateOptions()) = + getCollection(entity.javaClass).update(entity, where, options) + + @Suppress("SpellCheckingInspection") + suspend fun upsert(entity: T, where: Bson, options: UpdateOptions = UpdateOptions()) = + getCollection(entity.javaClass).upsert(entity, where, options) + + inline fun getCollection(): MongoOperator = getCollection(T::class.java) + + fun getCollection(clazz: Class): MongoOperator { + return operatorMap[clazz]?.uncheckedCast() ?: run { + val operator = MongoOperatorImpl(clazz, db, subscribeExecutor) + operatorMap[clazz] = operator + operator + } + } + + inline fun getOperator(collection: String) = getOperator(collection, T::class.java) + fun getOperator(collection: String, clazz: Class): MongoOperator { + return MongoOperatorImpl(getCollection(collection), clazz, subscribeExecutor) + } + + override fun toString(): String { + return "MongoTemplate(db=$db)" + } + + //override fun close() { + // //mongoClient.close() + //} +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoUtil.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoUtil.kt new file mode 100644 index 0000000..19bc5e0 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/MongoUtil.kt @@ -0,0 +1,117 @@ +package cn.tursom.database.mongodb + +import cn.tursom.core.isStatic +import cn.tursom.core.isTransient +import cn.tursom.core.uncheckedCast +import cn.tursom.database.mongodb.annotation.Collection +import cn.tursom.database.mongodb.annotation.Ignore +import org.bson.BsonValue +import org.bson.Document +import org.bson.conversions.Bson +import java.lang.reflect.Field +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.LinkedTransferQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.min +import kotlin.reflect.KProperty +import kotlin.reflect.jvm.javaField + +object MongoUtil { + private val fieldNameCache = ConcurrentHashMap() + private val mongoThreadId = AtomicInteger(0) + val mongoExecutor = getThreadPool(min(4, Runtime.getRuntime().availableProcessors())) + + fun collectionName(clazz: Class<*>): String { + return clazz.getAnnotation(Collection::class.java)?.name ?: clazz.simpleName + } + + fun fieldName(field: Field): String { + var fieldName = fieldNameCache[field] + if (fieldName == null) { + fieldName = field.getAnnotation(cn.tursom.database.mongodb.annotation.Field::class.java)?.name ?: field.name + fieldNameCache[field] = fieldName!! + } + return fieldName + } + + fun fieldName(property: KProperty<*>): String { + val javaField = property.javaField + return if (javaField != null) fieldName(javaField) else property.name + } + + fun convertToBson(entity: Any): Document { + return when (entity) { + is Map<*, *> -> entity.convert() + else -> { + val bson = Document() + entity.javaClass.declaredFields.filter { + it.isAccessible = true + !it.isStatic() + && !it.isTransient() + && it.getAnnotation(Ignore::class.java) == null + && (it.type != Lazy::class.java || it.get(entity).uncheckedCast>().isInitialized()) + }.forEach { + injectValue(bson, it.get(entity) ?: return@forEach, it) + } + bson + } + } + } + + fun injectValue(bson: Document, value: Any, field: Field) { + when (value) { + is Pair<*, *> -> bson[value.first?.toString() ?: return] = convertToBson(value.second ?: return) + is Map.Entry<*, *> -> bson[value.key?.toString() ?: return] = + convertToBson(value.value ?: return) + else -> bson[fieldName(field)] = value.convert() ?: return + } + } + + private fun Iterator<*>.convert(): List<*> { + val list = ArrayList() + forEach { + list.add(it.convert() ?: return@forEach) + } + return list + } + + private fun Map<*, *>.convert(): Document { + val doc = Document() + forEach { any, u -> + any ?: return@forEach + doc[any.toString()] = u.convert() ?: return@forEach + } + return doc + } + + private fun Any?.convert() = when (this) { + null -> null + is Enum<*> -> name + is Boolean, is Number, is String, is Bson, is BsonValue, is ByteArray -> this + is ShortArray -> asList() + is IntArray -> asList() + is LongArray -> asList() + is FloatArray -> asList() + is DoubleArray -> asList() + is BooleanArray -> asList() + is Map<*, *> -> this.convert() + is Iterator<*> -> this.convert() + is Iterable<*> -> this.iterator().convert() + else -> convertToBson(this) + } + + private fun getThreadPool(nThreads: Int) = ThreadPoolExecutor( + nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + LinkedTransferQueue(), + { + val thread = Thread(it) + thread.isDaemon = true + thread.name = "mongo-worker-${mongoThreadId.incrementAndGet()}" + thread + }, + ThreadPoolExecutor.CallerRunsPolicy() + ) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Projection.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Projection.kt new file mode 100644 index 0000000..ebb3b54 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Projection.kt @@ -0,0 +1,55 @@ +package cn.tursom.database.mongodb + +import com.mongodb.client.model.Projections +import org.bson.conversions.Bson +import kotlin.reflect.KProperty1 + +object Projection { + operator fun invoke(action: Projection.() -> Bson) = this.action() + + fun computed(field: KProperty1, expression: TExpression): Bson = + computed(MongoUtil.fieldName(field), expression) + + fun computed(fieldName: String, expression: TExpression): Bson = + Projections.computed(fieldName, expression) + + fun include(vararg fieldNames: KProperty1): Bson = + Projections.include(fieldNames.map { MongoUtil.fieldName(it) }) + + fun include(fieldNames: Collection>): Bson = + Projections.include(fieldNames.map { MongoUtil.fieldName(it) }) + + fun include(vararg fieldNames: String): Bson = Projections.include(fieldNames.asList()) + fun include(fieldNames: List): Bson = Projections.include(fieldNames) + + fun exclude(vararg fieldNames: KProperty1): Bson = + Projections.exclude(fieldNames.map { MongoUtil.fieldName(it) }) + + fun exclude(fieldNames: Collection>): Bson = + Projections.exclude(fieldNames.map { MongoUtil.fieldName(it) }) + + fun exclude(vararg fieldNames: String): Bson = Projections.exclude(fieldNames.asList()) + fun exclude(fieldNames: List): Bson = Projections.exclude(fieldNames) + + fun excludeId(): Bson = Projections.excludeId() + + fun exclude(field: KProperty1): Bson = Projections.elemMatch(MongoUtil.fieldName(field)) + fun exclude(field: KProperty1, filter: Bson): Bson = + Projections.elemMatch(MongoUtil.fieldName(field), filter) + + fun elemMatch(fieldName: String): Bson = Projections.elemMatch(fieldName) + fun elemMatch(fieldName: String, filter: Bson): Bson = Projections.elemMatch(fieldName, filter) + + fun metaTextScore(field: KProperty1): Bson = Projections.metaTextScore(MongoUtil.fieldName(field)) + fun metaTextScore(fieldName: String): Bson = Projections.metaTextScore(fieldName) + + fun slice(field: KProperty1, limit: Int): Bson = Projections.slice(MongoUtil.fieldName(field), limit) + fun slice(field: KProperty1, skip: Int, limit: Int): Bson = + Projections.slice(MongoUtil.fieldName(field), skip, limit) + + fun slice(fieldName: String, limit: Int): Bson = Projections.slice(fieldName, limit) + fun slice(fieldName: String, skip: Int, limit: Int): Bson = Projections.slice(fieldName, skip, limit) + + fun fields(vararg projections: Bson): Bson = Projections.fields(projections.asList()) + fun fields(projections: List): Bson = Projections.fields(projections) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Sort.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Sort.kt new file mode 100644 index 0000000..5193c4b --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Sort.kt @@ -0,0 +1,30 @@ +package cn.tursom.database.mongodb + +import com.mongodb.client.model.Sorts +import org.bson.conversions.Bson +import kotlin.reflect.KProperty1 + +object Sort { + operator fun invoke(action: Sort.() -> Bson) = this.action() + + fun ascending(vararg fieldNames: KProperty1): Bson = + Sorts.ascending(fieldNames.map { MongoUtil.fieldName(it) }) + + fun ascending(fieldNames: Collection>): Bson = + Sorts.ascending(fieldNames.map { MongoUtil.fieldName(it) }) + + fun ascending(vararg fieldNames: String): Bson = Sorts.ascending(fieldNames.asList()) + fun ascending(fieldNames: List): Bson = Sorts.ascending(fieldNames) + fun descending(vararg fieldNames: KProperty1): Bson = + Sorts.descending(fieldNames.map { MongoUtil.fieldName(it) }) + + fun descending(fieldNames: Collection>): Bson = + Sorts.descending(fieldNames.map { MongoUtil.fieldName(it) }) + + fun descending(vararg fieldNames: String): Bson = Sorts.descending(fieldNames.asList()) + fun descending(fieldNames: List): Bson = Sorts.descending(fieldNames) + fun KProperty1.metaTextScore(): Bson = Sorts.metaTextScore(MongoUtil.fieldName(this)) + fun metaTextScore(fieldName: String): Bson = Sorts.metaTextScore(fieldName) + fun orderBy(vararg sorts: Bson): Bson = Sorts.orderBy(sorts.asList()) + fun orderBy(sorts: List): Bson = Sorts.orderBy(sorts) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Update.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Update.kt new file mode 100644 index 0000000..de1bbe3 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Update.kt @@ -0,0 +1,34 @@ +package cn.tursom.database.mongodb + +import com.mongodb.client.model.Updates +import org.bson.conversions.Bson +import kotlin.reflect.KProperty + +object Update : MongoName { + operator fun invoke(action: Update.() -> Bson) = this.action() + + fun and(vararg update: Bson): Bson = Updates.combine(*update) + fun or(vararg update: Bson): Bson = Updates.combine(*update) + infix fun Bson.and(update: Bson): Bson = Updates.combine(this, update) + infix fun Bson.and(update: Update.() -> Bson): Bson = and(Update.update()) + fun combine(vararg updates: Bson): Bson = Updates.combine(*updates) + + + infix fun String.set(value: Any): Bson = when (value) { + is String, is Number, is Boolean -> Updates.set(this, value) + else -> Updates.set(this, MongoUtil.convertToBson(value)) + } + + infix fun KProperty<*>.set(value: Any): Bson = MongoUtil.fieldName(this) set value + + fun KProperty<*>.unset(): Bson = Updates.unset(MongoUtil.fieldName(this)) + fun setOnInsert(value: Bson): Bson = Updates.setOnInsert(value) + fun setOnInsert(value: Any): Bson = Updates.setOnInsert(MongoUtil.convertToBson(value)) + infix fun KProperty<*>.setOnInsert(value: Any): Bson = Updates.setOnInsert(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.rename(value: String): Bson = Updates.rename(MongoUtil.fieldName(this), value) + infix fun String.rename(value: String): Bson = Updates.rename(this, value) + infix fun KProperty<*>.inc(value: Number): Bson = Updates.inc(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.mul(value: Number): Bson = Updates.mul(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.min(value: Any): Bson = Updates.min(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.max(value: Any): Bson = Updates.max(MongoUtil.fieldName(this), value) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Where.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Where.kt new file mode 100644 index 0000000..ec55cd10 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/Where.kt @@ -0,0 +1,75 @@ +package cn.tursom.database.mongodb + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.TextSearchOptions +import org.bson.BsonType +import org.bson.conversions.Bson +import kotlin.reflect.KProperty + +@Suppress("unused") +object Where : MongoName { + inline operator fun invoke(action: Where.() -> Bson) = this.action() + + infix fun String.eq(value: Any): Bson = Filters.eq(this, value) + infix fun String.ne(value: Any): Bson = Filters.ne(this, value) + infix fun String.gt(value: Any): Bson = Filters.gt(this, value) + infix fun String.lt(value: Any): Bson = Filters.lt(this, value) + infix fun String.gte(value: Any): Bson = Filters.gte(this, value) + infix fun String.lte(value: Any): Bson = Filters.lte(this, value) + fun String.`in`(vararg value: Any): Bson = Filters.`in`(this, *value) + fun String.nin(vararg value: Any): Bson = Filters.nin(this, *value) + fun String.all(vararg value: Any): Bson = Filters.all(this, *value) + infix fun String.all(value: Iterable): Bson = Filters.all(this, value) + fun String.exists(exists: Boolean = true): Bson = Filters.exists(this, exists) + + infix fun String.type(type: String): Bson = Filters.type(this, type) + infix fun String.type(type: BsonType): Bson = Filters.type(this, type) + fun String.mod(divisor: Long, remainder: Long): Bson = Filters.mod(this, divisor, remainder) + infix fun String.regex(regex: String): Bson = Filters.regex(this, regex) + infix fun String.regex(regex: Regex): Bson = Filters.regex(this, regex.pattern) + fun String.regex(regex: String, option: String): Bson = Filters.regex(this, regex, option) + fun String.regex(regex: Regex, option: String): Bson = Filters.regex(this, regex.pattern, option) + + infix fun String.elemMatch(where: Bson): Bson = Filters.elemMatch(this, where) + infix fun String.elemMatch(where: Where.() -> Bson): Bson = Filters.elemMatch(this, Where.where()) + + infix fun KProperty<*>.eq(value: Any): Bson = Filters.eq(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.ne(value: Any): Bson = Filters.ne(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.gt(value: Any): Bson = Filters.gt(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.lt(value: Any): Bson = Filters.lt(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.gte(value: Any): Bson = Filters.gte(MongoUtil.fieldName(this), value) + infix fun KProperty<*>.lte(value: Any): Bson = Filters.lte(MongoUtil.fieldName(this), value) + fun KProperty<*>.`in`(vararg value: Any): Bson = Filters.`in`(MongoUtil.fieldName(this), *value) + fun KProperty<*>.nin(vararg value: Any): Bson = Filters.nin(MongoUtil.fieldName(this), *value) + fun KProperty<*>.all(vararg value: Any): Bson = Filters.all(MongoUtil.fieldName(this), *value) + infix fun KProperty<*>.all(value: Iterable): Bson = Filters.all(MongoUtil.fieldName(this), value) + fun KProperty<*>.exists(exists: Boolean = true): Bson = Filters.exists(MongoUtil.fieldName(this), exists) + + infix fun KProperty<*>.type(type: String): Bson = Filters.type(MongoUtil.fieldName(this), type) + infix fun KProperty<*>.type(type: BsonType): Bson = Filters.type(MongoUtil.fieldName(this), type) + fun KProperty<*>.mod(divisor: Long, remainder: Long): Bson = + Filters.mod(MongoUtil.fieldName(this), divisor, remainder) + + infix fun KProperty<*>.regex(regex: String): Bson = Filters.regex(MongoUtil.fieldName(this), regex) + infix fun KProperty<*>.regex(regex: Regex): Bson = Filters.regex(MongoUtil.fieldName(this), regex.pattern) + fun KProperty<*>.regex(regex: String, option: String): Bson = Filters.regex(MongoUtil.fieldName(this), regex, option) + fun KProperty<*>.regex(regex: Regex, option: String): Bson = + Filters.regex(MongoUtil.fieldName(this), regex.pattern, option) + + infix fun KProperty<*>.elemMatch(where: Bson): Bson = MongoUtil.fieldName(this) elemMatch where + infix fun KProperty<*>.elemMatch(where: Where.() -> Bson): Bson = MongoUtil.fieldName(this) elemMatch where + + fun text(search: String): Bson = Filters.text(search) + fun text(search: String, option: TextSearchOptions): Bson = Filters.text(search, option) + fun where(javaScriptExpression: String): Bson = Filters.where(javaScriptExpression) + fun expr(expression: Any): Bson = Filters.expr(expression) + + fun and(vararg bson: Bson): Bson = Filters.and(*bson) + infix fun Bson.and(bson: Bson): Bson = Filters.and(this, bson) + fun or(vararg bson: Bson): Bson = Filters.or(*bson) + infix fun Bson.or(bson: Bson): Bson = Filters.or(this, bson) + fun nor(vararg bson: Bson): Bson = Filters.or(*bson) + infix fun Bson.nor(bson: Bson): Bson = Filters.nor(this, bson) + + operator fun Bson.not(): Bson = Filters.not(this) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Collection.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Collection.kt new file mode 100644 index 0000000..89983f4 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Collection.kt @@ -0,0 +1,6 @@ +package cn.tursom.database.mongodb.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class Collection(val name: String) + diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Field.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Field.kt new file mode 100644 index 0000000..c056a39 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Field.kt @@ -0,0 +1,6 @@ +package cn.tursom.database.mongodb.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Field(val name: String) + diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Ignore.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Ignore.kt new file mode 100644 index 0000000..f3e38c5 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/annotation/Ignore.kt @@ -0,0 +1,5 @@ +package cn.tursom.database.mongodb.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Ignore(val name: String) \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/AsyncInsertMongoOperator.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/AsyncInsertMongoOperator.kt new file mode 100644 index 0000000..d46737b --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/AsyncInsertMongoOperator.kt @@ -0,0 +1,95 @@ +package cn.tursom.database.mongodb.operator + +import cn.tursom.log.Slf4j +import cn.tursom.log.impl.Slf4jImpl +import com.mongodb.client.model.InsertManyOptions +import com.mongodb.client.model.InsertOneOptions +import com.mongodb.client.result.InsertOneResult +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import org.bson.Document +import java.util.concurrent.atomic.AtomicInteger + +class AsyncInsertMongoOperator( + private val mongoOperator: MongoOperator, +) : MongoOperator by mongoOperator { + companion object : Slf4j by Slf4jImpl() { + private val acknowledged = InsertOneResult.acknowledged(null) + } + + private val wroteAtomic = AtomicInteger(0) + private val documentChannel: Channel = Channel(1024 * 256) + + val wrote get() = wroteAtomic.get() + + private val job = GlobalScope.launch { + repeat(32) { + launch { + var list = ArrayList() + var lastSave = System.currentTimeMillis() + suspend fun save() { + if (list.isEmpty()) return + if (logger.isDebugEnabled) { + logger.debug("begin to save document: {}", list) + } + var saveDocument = mongoOperator.saveDocument(list, InsertManyOptions().ordered(false)) + var retryTimes = 0 + while (saveDocument?.wasAcknowledged() != true && retryTimes < 3) { + retryTimes++ + delay(100) + saveDocument = mongoOperator.saveDocument(list) + } + list = ArrayList() + lastSave = System.currentTimeMillis() + saveDocument ?: return + wroteAtomic.addAndGet(saveDocument.insertedIds.size) + if (logger.isDebugEnabled) { + logger.debug("save result: {}", saveDocument) + } + } + @Suppress("EXPERIMENTAL_API_USAGE") + while (!documentChannel.isClosedForReceive) { + try { + list.add(withTimeout(100) { documentChannel.receive() }) + } catch (e: TimeoutCancellationException) { + } catch (e: ClosedReceiveChannelException) { + break + } + if (list.size >= 1024 || System.currentTimeMillis() - lastSave > 100) { + save() + } + } + if (list.size > 0) { + save() + } + } + } + } + + override suspend fun save(entity: T, options: InsertOneOptions): Boolean { + //insertOne(entity, options) + if (logger.isDebugEnabled) { + logger.debug("save object: {}, options: {}, result: {}", entity, options, insertOne(entity, options)) + } + return true + } + + override suspend fun saveDocument(document: Document, options: InsertOneOptions): InsertOneResult? { + documentChannel.send(document) + return acknowledged + } + + override suspend fun insertOne(entity: T, options: InsertOneOptions): InsertOneResult? { + documentChannel.send(convertToBson(entity)) + return acknowledged + } + + override fun close() { + documentChannel.close() + } + + suspend fun join() { + job.join() + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/CachedInsertMongoOperator.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/CachedInsertMongoOperator.kt new file mode 100644 index 0000000..385ebea --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/CachedInsertMongoOperator.kt @@ -0,0 +1,104 @@ +package cn.tursom.database.mongodb.operator + +import com.mongodb.client.model.InsertManyOptions +import com.mongodb.client.model.InsertOneOptions +import com.mongodb.client.result.InsertOneResult +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import org.bson.Document +import java.util.concurrent.atomic.AtomicInteger + +class CachedInsertMongoOperator( + private val mongoOperator: MongoOperator, + private val ordered: Boolean = true, + private val maxSaveCount: Int = 4096, + private val maxDelayTimeMS: Long = 100, +) : MongoOperator by mongoOperator { + companion object { + private val unacknowledged = InsertOneResult.unacknowledged() + } + + private val documentChannel: Channel, Document>> = Channel(1024 * 256) + private val wroteAtomic = AtomicInteger(0) + private val received = AtomicInteger(0) + val wrote get() = wroteAtomic.get() + + private val job = GlobalScope.launch { + repeat(32) { + launch { + var list = ArrayList, Document>>() + var lastSave = System.currentTimeMillis() + suspend fun save() { + var insertManyResult = saveDocument( + list.map(Pair, Document>::second), + InsertManyOptions().ordered(ordered) + ) + var retryTimes = 0 + while (insertManyResult?.wasAcknowledged() != true && retryTimes < 3) { + retryTimes++ + delay(100) + insertManyResult = saveDocument( + list.map(Pair, Document>::second), + InsertManyOptions().ordered(ordered) + ) + } + if (insertManyResult?.wasAcknowledged() == true) { + wroteAtomic.addAndGet(insertManyResult.insertedIds.size) + list.forEachIndexed { index, (channel) -> + channel.send(InsertOneResult.acknowledged(insertManyResult.insertedIds[index])) + } + } else { + list.forEach { (channel) -> + channel.send(unacknowledged) + } + } + list = ArrayList() + lastSave = System.currentTimeMillis() + } + @Suppress("EXPERIMENTAL_API_USAGE") + while (!documentChannel.isClosedForReceive) { + try { + list.add(documentChannel.poll() ?: withTimeout(maxDelayTimeMS) { documentChannel.receive() }) + received.incrementAndGet() + } catch (e: TimeoutCancellationException) { + } catch (e: ClosedReceiveChannelException) { + //println("closed") + break + } + if (list.size >= maxSaveCount || System.currentTimeMillis() - lastSave > maxDelayTimeMS) { + save() + } + } + if (list.size > 0) { + save() + } + } + } + } + + override suspend fun save(entity: T, options: InsertOneOptions): Boolean { + return insertOne(entity, options)?.wasAcknowledged() ?: false + } + + override suspend fun saveDocument(document: Document, options: InsertOneOptions): InsertOneResult? { + val channel = Channel() + documentChannel.send(channel to document) + return channel.receive() + } + + override suspend fun insertOne(entity: T, options: InsertOneOptions): InsertOneResult? { + val channel = Channel() + documentChannel.send(channel to convertToBson(entity)) + return channel.receive() + } + + override fun close() { + documentChannel.close() + mongoOperator.close() + } + + suspend fun join() { + job.join() + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicCachedInsertMongoOperator.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicCachedInsertMongoOperator.kt new file mode 100644 index 0000000..4549efe --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicCachedInsertMongoOperator.kt @@ -0,0 +1,45 @@ +package cn.tursom.database.mongodb.operator + +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +@Suppress("FunctionName", "unused") +fun DynamicCachedInsertMongoOperator( + mongoOperator: MongoOperator, + executorService: ScheduledExecutorService, + loadCheckDelay: Long = 100, + loadCheckDelayTimeUnit: TimeUnit = TimeUnit.MILLISECONDS, + ordered: Boolean = true, + maxSaveCount: Int = 4096, + maxDelayTimeMS: Long = 100, + mainTransLoad: Int = 10, + spareTransLoad: Int = 50, + transWaitCount: Int = 10, +) = DynamicInsertMongoOperator( + mongoOperator, + CachedInsertMongoOperator(mongoOperator, ordered, maxSaveCount, maxDelayTimeMS), + executorService, + loadCheckDelay, + loadCheckDelayTimeUnit, + object : (DynamicInsertMongoOperator.WorkType, Int) -> DynamicInsertMongoOperator.WorkType { + private var workLoop = AtomicInteger(0) + override fun invoke(workType: DynamicInsertMongoOperator.WorkType, load: Int): DynamicInsertMongoOperator.WorkType { + when (workType) { + DynamicInsertMongoOperator.WorkType.MAIN -> if (load > spareTransLoad) { + if (workLoop.incrementAndGet() > transWaitCount) { + workLoop.set(0) + return DynamicInsertMongoOperator.WorkType.SPARE + } + } + DynamicInsertMongoOperator.WorkType.SPARE -> if (load < mainTransLoad) { + if (workLoop.incrementAndGet() > transWaitCount) { + workLoop.set(0) + return DynamicInsertMongoOperator.WorkType.MAIN + } + } + } + return workType + } + } +) diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicInsertMongoOperator.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicInsertMongoOperator.kt new file mode 100644 index 0000000..d3e126a --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/DynamicInsertMongoOperator.kt @@ -0,0 +1,62 @@ +package cn.tursom.database.mongodb.operator + +import com.mongodb.client.model.InsertOneOptions +import com.mongodb.client.result.InsertOneResult +import org.bson.Document +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +class DynamicInsertMongoOperator( + private val mainMongoOperator: MongoOperator, + private val spareMongoOperator: MongoOperator, + private val executorService: ScheduledExecutorService, + loadCheckDelay: Long = 100, + loadCheckDelayTimeUnit: TimeUnit = TimeUnit.MILLISECONDS, + private val workTypeCheck: (workType: WorkType, load: Int) -> WorkType, +) : MongoOperator by mainMongoOperator { + enum class WorkType { + MAIN, SPARE + } + + var workType = WorkType.MAIN + private val workLoad = AtomicInteger(0) + + init { + executorService.scheduleWithFixedDelay({ + workType = workTypeCheck(workType, workLoad.getAndSet(0)) + }, loadCheckDelay, loadCheckDelay, loadCheckDelayTimeUnit) + } + + private val workMongoOperator + get() = when (workType) { + WorkType.MAIN -> mainMongoOperator + WorkType.SPARE -> spareMongoOperator + } + + override suspend fun save(entity: T, options: InsertOneOptions): Boolean { + workLoad.incrementAndGet() + return workMongoOperator.save(entity, options) + } + + override suspend fun saveDocument(document: Document, options: InsertOneOptions): InsertOneResult? { + workLoad.incrementAndGet() + return workMongoOperator.saveDocument(document, options) + } + + override suspend fun insertOne(entity: T, options: InsertOneOptions): InsertOneResult? { + workLoad.incrementAndGet() + return workMongoOperator.insertOne(entity, options) + } + + override fun close() { + try { + mainMongoOperator.close() + } catch (e: Exception) { + } + try { + spareMongoOperator.close() + } catch (e: Exception) { + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperator.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperator.kt new file mode 100644 index 0000000..efee97b --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperator.kt @@ -0,0 +1,115 @@ +package cn.tursom.database.mongodb.operator + +import cn.tursom.core.datastruct.AsyncIterator +import cn.tursom.database.mongodb.BsonFactory +import cn.tursom.database.mongodb.IndexBuilder +import cn.tursom.database.mongodb.Update +import cn.tursom.database.mongodb.Where +import com.mongodb.client.model.* +import com.mongodb.client.result.InsertManyResult +import com.mongodb.client.result.InsertOneResult +import com.mongodb.client.result.UpdateResult +import org.bson.Document +import org.bson.conversions.Bson +import java.io.Closeable +import kotlin.reflect.KProperty1 + +interface MongoOperator : BsonFactory, Closeable { + suspend fun save(entity: T, options: InsertOneOptions = InsertOneOptions()): Boolean { + return insertOne(entity, options)?.wasAcknowledged() ?: false + } + + suspend fun save(entities: Collection, options: InsertManyOptions = InsertManyOptions()) { + insertMany(entities, options) + } + + suspend fun saveDocument(document: Document, options: InsertOneOptions = InsertOneOptions()): InsertOneResult? + + suspend fun saveDocument( + documents: List, + options: InsertManyOptions = InsertManyOptions() + ): InsertManyResult? + + suspend fun insertOne(entity: T, options: InsertOneOptions = InsertOneOptions()): InsertOneResult? { + return saveDocument(convertToBson(entity)) + } + + suspend fun insertMany(entities: Collection, options: InsertManyOptions = InsertManyOptions()): InsertManyResult? { + return saveDocument(entities.map { convertToBson(it) }, options) + } + + suspend fun updateMulti(update: Bson, where: Bson, options: UpdateOptions = UpdateOptions()): UpdateResult? + + suspend fun update(update: Bson, where: Bson, options: UpdateOptions = UpdateOptions()): UpdateResult? + + suspend fun update(entity: T, where: Bson, options: UpdateOptions = UpdateOptions()): UpdateResult? { + return update(convertToBson(entity), where, options) + } + + @Suppress("SpellCheckingInspection") + suspend fun upsert(entity: T, where: Bson, options: UpdateOptions = UpdateOptions()): UpdateResult? { + return update(entity, where, options.upsert(true)) + } + + @Suppress("SpellCheckingInspection") + suspend fun upsert(update: Bson, where: Bson, options: UpdateOptions = UpdateOptions()): UpdateResult? { + return update(update, where, options.upsert(true)) + } + + @Suppress("SpellCheckingInspection") + suspend fun upsert( + update: Update.() -> Bson, + where: Where.() -> Bson, + options: UpdateOptions = UpdateOptions(), + ): UpdateResult? { + return upsert(Update.update(), Where.where(), options) + } + + suspend fun add( + field: KProperty1, + value: Number, + where: Bson, + options: UpdateOptions = UpdateOptions(), + ): UpdateResult? { + return upsert( + Update { field inc value }, + where, options + ) + } + + suspend fun inc(field: KProperty1, where: Bson): UpdateResult? { + return add(field, 1, where) + } + + suspend fun getOne(where: Bson? = null): T? + + suspend fun list(where: Bson? = null, skip: Int = 0, limit: Int = 0): List + + suspend fun listDocument(where: Bson? = null, skip: Int = 0, limit: Int = 0): List + + fun get(where: Bson? = null, bufSize: Int = 32): AsyncIterator + + fun getDocument(where: Bson? = null, bufSize: Int = 32, skip: Int = 0, limit: Int = 0): AsyncIterator + + fun aggregate(vararg pipeline: Bson, bufSize: Int = 32): AsyncIterator + + fun delete(entity: T, options: DeleteOptions = DeleteOptions()) + + suspend fun saveIfNotExists(entity: T) { + val document = convertToBson(entity) + upsert(document, document) + } + + suspend fun count(): Long + suspend fun count(where: Bson): Long + + suspend fun createIndexSuspend(key: Bson, indexOptions: IndexOptions = IndexOptions()): String? + + suspend fun createIndexSuspend(indexBuilder: IndexBuilder.() -> Unit): String? { + val builder = IndexBuilder() + builder.indexBuilder() + return createIndexSuspend(builder.indexDocument, builder.indexOption) + } + + override fun close() {} +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperatorImpl.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperatorImpl.kt new file mode 100644 index 0000000..3186db9 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/operator/MongoOperatorImpl.kt @@ -0,0 +1,241 @@ +package cn.tursom.database.mongodb.operator + +import cn.tursom.core.datastruct.AsyncIterator +import cn.tursom.database.mongodb.* +import cn.tursom.database.mongodb.subscriber.* +import cn.tursom.log.Slf4j +import cn.tursom.log.impl.Slf4jImpl +import com.mongodb.client.model.* +import com.mongodb.client.result.InsertManyResult +import com.mongodb.client.result.InsertOneResult +import com.mongodb.client.result.UpdateResult +import com.mongodb.reactivestreams.client.MongoCollection +import com.mongodb.reactivestreams.client.MongoDatabase +import org.bson.Document +import org.bson.conversions.Bson +import org.reactivestreams.Publisher +import java.util.concurrent.Executor +import kotlin.coroutines.suspendCoroutine +import kotlin.reflect.KProperty1 + +@Suppress("MemberVisibilityCanBePrivate") +class MongoOperatorImpl( + @Suppress("MemberVisibilityCanBePrivate") val collection: MongoCollection, + clazz: Class, + private val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : MongoOperator, MongoCollection by collection, BsonFactoryImpl(clazz) { + companion object : Slf4j by Slf4jImpl() + + constructor( + clazz: Class, + db: MongoDatabase, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, + ) : this(db.getCollection(MongoUtil.collectionName(clazz)), clazz, subscribeExecutor) + + override suspend fun save(entity: T, options: InsertOneOptions): Boolean { + return insertOne(entity, options)?.wasAcknowledged() ?: false + } + + override suspend fun save(entities: Collection, options: InsertManyOptions) { + insertMany(entities, options) + } + + override suspend fun saveDocument(document: Document, options: InsertOneOptions): InsertOneResult? { + return suspendCoroutine { cont -> + insertOne(document, options).subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } + } + + override suspend fun saveDocument(documents: List, options: InsertManyOptions): InsertManyResult? { + val publisher = collection.insertMany(documents, options) + return suspendCoroutine { cont -> + publisher.subscribe( + SuspendInsertOneSubscriber( + cont, + documents.size.toLong(), + subscribeExecutor = subscribeExecutor + ) + ) + } + } + + override suspend fun insertOne(entity: T, options: InsertOneOptions): InsertOneResult? { + return saveDocument(convertToBson(entity)) + } + + override suspend fun insertMany(entities: Collection, options: InsertManyOptions): InsertManyResult? { + return saveDocument(entities.map { convertToBson(it) }, options) + //val publisher = collection.insertMany(entities.map { convertToBson(it) }, options) + //return suspendCoroutine { cont -> + // publisher.subscribe(SuspendInsertListSubscriber(cont, entities.size.toLong(), subscribeExecutor = subscribeExecutor)) + //} + } + + override suspend fun updateMulti(update: Bson, where: Bson, options: UpdateOptions): UpdateResult? { + val publisher = collection.updateMany(where, update, options) + return suspendCoroutine { cont -> + publisher.subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } + } + + override suspend fun update(update: Bson, where: Bson, options: UpdateOptions): UpdateResult? { + val publisher = collection.updateOne(where, update, options) + return suspendCoroutine { cont -> + publisher.subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } + } + + override suspend fun update(entity: T, where: Bson, options: UpdateOptions): UpdateResult? { + return update(convertToBson(entity), where, options) + } + + @Suppress("SpellCheckingInspection") + override suspend fun upsert(entity: T, where: Bson, options: UpdateOptions): UpdateResult? { + return update(entity, where, options.upsert(true)) + } + + @Suppress("SpellCheckingInspection") + override suspend fun upsert(update: Bson, where: Bson, options: UpdateOptions): UpdateResult? { + return update(update, where, options.upsert(true)) + } + + @Suppress("SpellCheckingInspection") + override suspend fun upsert( + update: Update.() -> Bson, + where: Where.() -> Bson, + options: UpdateOptions, + ): UpdateResult? { + return upsert(Update.update(), Where.where(), options) + } + + override suspend fun add( + field: KProperty1, + value: Number, + where: Bson, + options: UpdateOptions, + ): UpdateResult? { + return upsert( + Update { field inc value }, + where, options + ) + } + + override suspend fun inc(field: KProperty1, where: Bson): UpdateResult? { + return add(field, 1, where) + } + + override suspend fun getOne(where: Bson?): T? { + val publisher = if (where == null) find() else find(where) + return suspendCoroutine { cont -> + publisher.subscribe(SuspendOneSubscriber(this, cont, subscribeExecutor = subscribeExecutor)) + } + } + + override suspend fun list(where: Bson?, skip: Int, limit: Int): List { + val publisher = if (where == null) find() else find(where) + publisher.skip(skip) + if (limit > 0) { + publisher.limit(limit) + } + return suspendCoroutine { cont -> + publisher.subscribe( + SuspendListSubscriber( + this, + cont, + if (limit > 0) limit.toLong() else Long.MAX_VALUE, + subscribeExecutor = subscribeExecutor + ) + ) + } + } + + override suspend fun listDocument(where: Bson?, skip: Int, limit: Int): List { + println("skip: $skip, limit: $limit") + val publisher = if (where == null) find() else find(where) + publisher.skip(skip) + if (limit > 0) { + publisher.limit(limit) + } + return suspendCoroutine { cont -> + publisher.subscribe( + SuspendListDocumentSubscriber( + cont, + if (limit > 0) limit.toLong() else Long.MAX_VALUE, + subscribeExecutor = subscribeExecutor + ) + ) + } + } + + override fun get(where: Bson?, bufSize: Int): AsyncIterator { + val find = if (where == null) find() else find(where) + return iterator(find, bufSize) + } + + override fun getDocument(where: Bson?, bufSize: Int, skip: Int, limit: Int): AsyncIterator { + val find = if (where == null) find() else find(where) + find.skip(skip) + if (limit > 0) { + find.limit(limit) + } + return documentIterator(find, bufSize) + } + + override fun aggregate(vararg pipeline: Bson, bufSize: Int): AsyncIterator = + iterator(aggregate(pipeline.asList()), bufSize) + + private fun iterator(publisher: Publisher, bufSize: Int = 32): AsyncIterator { + val subscriber = AsyncIteratorSubscriber( + this, + bufSize, + subscribeExecutor = subscribeExecutor + ) + publisher.subscribe(subscriber) + return subscriber + } + + private fun documentIterator(publisher: Publisher, bufSize: Int = 32): AsyncIterator { + val subscriber = AsyncDocumentIteratorSubscriber(bufSize, subscribeExecutor = subscribeExecutor) + publisher.subscribe(subscriber) + return subscriber + } + + override fun delete(entity: T, options: DeleteOptions) { + deleteOne(convertToBson(entity), options) + } + + override suspend fun saveIfNotExists(entity: T) { + val document = convertToBson(entity) + upsert(document, document) + } + + override suspend fun count(): Long { + val publisher = countDocuments() + return suspendCoroutine { cont -> + publisher.subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } ?: 0L + } + + override suspend fun count(where: Bson): Long { + val publisher = countDocuments(where) + return suspendCoroutine { cont -> + publisher.subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } ?: 0L + } + + override suspend fun createIndexSuspend(key: Bson, indexOptions: IndexOptions): String? { + return suspendCoroutine { cont -> + createIndex(key, indexOptions).subscribe(SuspendInsertOneSubscriber(cont, subscribeExecutor = subscribeExecutor)) + } + } + + override suspend fun createIndexSuspend(indexBuilder: IndexBuilder.() -> Unit): String? { + val builder = IndexBuilder() + builder.indexBuilder() + return createIndexSuspend(builder.indexDocument, builder.indexOption) + } + + override fun toString(): String { + return "MongoOperator(collection=$collection, clazz=$clazz)" + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractDocumentSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractDocumentSubscriber.kt new file mode 100644 index 0000000..67ed562 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractDocumentSubscriber.kt @@ -0,0 +1,44 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.database.mongodb.MongoUtil +import org.bson.Document +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.Executor + +@Suppress("ReactiveStreamsSubscriberImplementation") +abstract class AbstractDocumentSubscriber( + val size: Long = 1, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber { + abstract fun next(t: Document) + + private var requestRemain = 0L + protected var requested = 0 + protected lateinit var s: Subscription + protected var compete = false + + override fun onComplete() { + compete = true + } + + override fun onSubscribe(s: Subscription) { + this.s = s + requestRemain = size - requested + subscribeExecutor.execute { + s.request(requestRemain) + } + } + + override fun onNext(t: Document) { + next(t) + requestRemain-- + requested++ + if (requestRemain > 0) return + if (size - requested > 0) { + s.request(size - requested) + } else { + onComplete() + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractSubscriber.kt new file mode 100644 index 0000000..5f1ddfb --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AbstractSubscriber.kt @@ -0,0 +1,46 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.database.mongodb.BsonFactory +import cn.tursom.database.mongodb.MongoUtil +import org.bson.Document +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.Executor + +@Suppress("ReactiveStreamsSubscriberImplementation") +abstract class AbstractSubscriber( + private val bsonFactory: BsonFactory, + val size: Long = 1, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber { + abstract fun next(t: T) + + private var requestRemain = 0L + protected var requested = 0 + protected lateinit var s: Subscription + protected var compete = false + + override fun onComplete() { + compete = true + } + + override fun onSubscribe(s: Subscription) { + this.s = s + requestRemain = size - requested + subscribeExecutor.execute { + s.request(requestRemain) + } + } + + override fun onNext(t: Document) { + next(bsonFactory.parse(t)) + requestRemain-- + requested++ + if (requestRemain > 0) return + if (size - requested > 0) { + s.request(size - requested) + } else { + onComplete() + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncDocumentIteratorSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncDocumentIteratorSubscriber.kt new file mode 100644 index 0000000..55da8bd --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncDocumentIteratorSubscriber.kt @@ -0,0 +1,72 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.core.Disposable +import cn.tursom.core.datastruct.AsyncIterator +import cn.tursom.database.mongodb.MongoUtil +import org.bson.Document +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Suppress("ReactiveStreamsSubscriberImplementation") +class AsyncDocumentIteratorSubscriber( + val bufSize: Int = 32, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber, AsyncIterator { + protected lateinit var s: Subscription + + @Volatile + protected var compete = false + + private var cont = Disposable>() + private var notify = Disposable>() + private val cache = ConcurrentLinkedQueue() + + override fun onComplete() { + cont.get()?.resumeWithException(Exception()) + notify.get()?.resume(Unit) + compete = true + } + + override fun onNext(t: Document) { + cont.get()?.resume(t) ?: cache.add(t) + notify.get()?.resume(Unit) + } + + override fun onSubscribe(s: Subscription) { + this.s = s + subscribeExecutor.execute { + s.request(1) + } + } + + override fun onError(t: Throwable) { + cont.get()?.resumeWithException(t) ?: t.printStackTrace() + } + + override suspend fun next(): Document { + return cache.poll() ?: suspendCoroutine { cont -> + this.cont.set(cont) + s.request(bufSize.toLong()) + cache.poll()?.let { this.cont.get()?.resume(it) ?: cache.add(it) } + } + } + + override suspend fun hasNext(): Boolean { + if (cache.isEmpty() && !compete) { + suspendCoroutine { + notify.set(it) + s.request(bufSize.toLong()) + if (cache.isNotEmpty()) { + notify.get()?.resume(Unit) + } + } + } + return cache.isNotEmpty() + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncIteratorSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncIteratorSubscriber.kt new file mode 100644 index 0000000..562ae2c --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/AsyncIteratorSubscriber.kt @@ -0,0 +1,78 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.core.Disposable +import cn.tursom.core.datastruct.AsyncIterator +import cn.tursom.database.mongodb.BsonFactory +import cn.tursom.database.mongodb.MongoUtil +import org.bson.Document +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Suppress("ReactiveStreamsSubscriberImplementation") +class AsyncIteratorSubscriber( + private val bsonFactory: BsonFactory, + val bufSize: Int = 32, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber, AsyncIterator, BsonFactory by bsonFactory { + protected lateinit var s: Subscription + + @Volatile + protected var compete = false + + private var cont = Disposable>() + private var notify = Disposable>() + private val cache = ConcurrentLinkedQueue() + + override fun onComplete() { + cont.get()?.resumeWithException(Exception()) + notify.get()?.resume(Unit) + compete = true + } + + override fun onNext(t: Document) { + next(bsonFactory.parse(t)) + } + + override fun onSubscribe(s: Subscription) { + this.s = s + subscribeExecutor.execute { + s.request(1) + } + } + + fun next(t: T) { + cont.get()?.resume(t) ?: cache.add(t) + notify.get()?.resume(Unit) + } + + override fun onError(t: Throwable) { + cont.get()?.resumeWithException(t) ?: t.printStackTrace() + } + + override suspend fun next(): T { + return cache.poll() ?: suspendCoroutine { cont -> + this.cont.set(cont) + s.request(bufSize.toLong()) + cache.poll()?.let { this.cont.get()?.resume(it) ?: cache.add(it) } + } + } + + override suspend fun hasNext(): Boolean { + if (cache.isEmpty() && !compete) { + suspendCoroutine { + notify.set(it) + s.request(bufSize.toLong()) + if (cache.isNotEmpty()) { + notify.get()?.resume(Unit) + } + } + } + return cache.isNotEmpty() + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertListSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertListSubscriber.kt new file mode 100644 index 0000000..84af030 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertListSubscriber.kt @@ -0,0 +1,34 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.database.mongodb.MongoUtil +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("ReactiveStreamsSubscriberImplementation") +class SuspendInsertListSubscriber( + val cont: Continuation>, + val requestSize: Long = Long.MAX_VALUE, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber { + val resultList = ArrayList() + override fun onComplete() { + cont.resume(resultList) + } + + override fun onSubscribe(s: Subscription) { + if (requestSize > 0) subscribeExecutor.execute { s.request(requestSize) } + else onComplete() + } + + override fun onNext(t: T) { + resultList.add(t) + } + + override fun onError(t: Throwable) { + cont.resumeWithException(t) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertOneSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertOneSubscriber.kt new file mode 100644 index 0000000..f0b56e7 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendInsertOneSubscriber.kt @@ -0,0 +1,34 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.core.Disposable +import cn.tursom.database.mongodb.MongoUtil +import org.reactivestreams.Subscriber +import org.reactivestreams.Subscription +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("ReactiveStreamsSubscriberImplementation") +class SuspendInsertOneSubscriber( + cont: Continuation, + val requestSize: Long = 1, + val subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : Subscriber { + val contDisposable = Disposable(cont) + override fun onComplete() { + contDisposable.get()?.resume(null) + } + + override fun onSubscribe(s: Subscription) = if (requestSize > 0) { + subscribeExecutor.execute { s.request(requestSize) } + } else onComplete() + + override fun onNext(t: T) { + contDisposable.get()?.resume(t) + } + + override fun onError(t: Throwable) { + contDisposable.get()?.resumeWithException(t) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListDocumentSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListDocumentSubscriber.kt new file mode 100644 index 0000000..d4a075c --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListDocumentSubscriber.kt @@ -0,0 +1,27 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.database.mongodb.MongoUtil +import org.bson.Document +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("ReactiveStreamsSubscriberImplementation") +class SuspendListDocumentSubscriber( + val cont: Continuation>, + size: Long = Long.MAX_VALUE, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : AbstractDocumentSubscriber(size, subscribeExecutor) { + companion object { + const val maxDefaultArrayCache = 4096 + } + + val resultList = ArrayList(if (size < maxDefaultArrayCache) size.toInt() else maxDefaultArrayCache) + override fun onComplete() = cont.resume(resultList) + override fun onError(t: Throwable) = cont.resumeWithException(t) + + override fun next(t: Document) { + resultList.add(t) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListSubscriber.kt new file mode 100644 index 0000000..8b84034 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendListSubscriber.kt @@ -0,0 +1,28 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.database.mongodb.BsonFactory +import cn.tursom.database.mongodb.MongoUtil +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("ReactiveStreamsSubscriberImplementation") +class SuspendListSubscriber( + bsonFactory: BsonFactory, + val cont: Continuation>, + size: Long = Long.MAX_VALUE, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : AbstractSubscriber(bsonFactory, size, subscribeExecutor) { + companion object { + const val maxDefaultArrayCache = 4096 + } + + val resultList = ArrayList(if (size < maxDefaultArrayCache) size.toInt() else maxDefaultArrayCache) + override fun onComplete() = cont.resume(resultList) + override fun onError(t: Throwable) = cont.resumeWithException(t) + + override fun next(t: T) { + resultList.add(t) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendOneSubscriber.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendOneSubscriber.kt new file mode 100644 index 0000000..56055ba --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/subscriber/SuspendOneSubscriber.kt @@ -0,0 +1,30 @@ +package cn.tursom.database.mongodb.subscriber + +import cn.tursom.core.Disposable +import cn.tursom.database.mongodb.BsonFactory +import cn.tursom.database.mongodb.MongoUtil +import java.util.concurrent.Executor +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Suppress("ReactiveStreamsSubscriberImplementation") +class SuspendOneSubscriber( + bsonFactory: BsonFactory, + cont: Continuation, + size: Long = 1, + subscribeExecutor: Executor = MongoUtil.mongoExecutor, +) : AbstractSubscriber(bsonFactory, size, subscribeExecutor) { + val contDisposable = Disposable(cont) + override fun onComplete() { + contDisposable.get()?.resume(null) + } + + override fun next(t: T) { + contDisposable.get()?.resume(t) + } + + override fun onError(t: Throwable) { + contDisposable.get()?.resumeWithException(t) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/utils.kt b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/utils.kt new file mode 100644 index 0000000..afbc283 --- /dev/null +++ b/ts-database/ts-mongodb/src/main/kotlin/cn/tursom/database/mongodb/utils.kt @@ -0,0 +1,5 @@ +package cn.tursom.database.mongodb + +import com.mongodb.reactivestreams.client.MongoClient + +fun MongoClient.getMongoTemplate(db: String) = MongoTemplate(this, db) \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/build.gradle.kts b/ts-database/ts-mongodb/ts-mongodb-spring/build.gradle.kts new file mode 100644 index 0000000..b3a14ab --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + kotlin("jvm") + `maven-publish` +} + +dependencies { + api(project(":")) + implementation(project(":ts-core")) + compileOnly(group = "org.springframework.data", name = "spring-data-mongodb", version = "3.1.7") +} + +@kotlin.Suppress("UNCHECKED_CAST") +(rootProject.ext["excludeTest"] as (Project, TaskContainer) -> Unit)(project, tasks) + +tasks.register("install") { + finalizedBy(tasks["publishToMavenLocal"]) +} + +publishing { + publications { + create("maven") { + groupId = project.group.toString() + artifactId = project.name + version = project.version.toString() + + from(components["java"]) + try { + artifact(tasks["sourcesJar"]) + } catch (e: Exception) { + } + } + } +} diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/AggregationBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/AggregationBuilder.kt new file mode 100644 index 0000000..aab8781 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/AggregationBuilder.kt @@ -0,0 +1,95 @@ +package cn.tursom.database.mongodb.spring + +import org.springframework.data.mongodb.core.aggregation.Aggregation +import org.springframework.data.mongodb.core.aggregation.AggregationOperation +import kotlin.reflect.KProperty + +@Suppress("unused") +object AggregationBuilder : MongoName { + inline infix operator fun invoke( + operatorBuilder: AggregationBuilder.(MutableList) -> MutableList + ): Aggregation { + val operator = this.operatorBuilder(ArrayList()) + return Aggregation.newAggregation(operator) + } + + infix operator fun MutableList.plus( + operation: AggregationOperation + ): MutableList { + add(operation) + return this + } + + infix fun MutableList.match( + builder: QueryBuilder.() -> Unit + ): MutableList { + add(Aggregation.match(QueryBuilder criteria builder)) + return this + } + + infix fun MutableList.group( + builder: GroupBuilder.() -> AggregationOperation + ): MutableList { + add(GroupBuilder.builder()) + return this + } + + infix fun MutableList.project( + builder: ProjectBuilder.() -> AggregationOperation + ): MutableList { + add(ProjectBuilder.builder()) + return this + } + + infix fun MutableList.unwind( + field: String + ): MutableList { + add(Aggregation.unwind(field)) + return this + } + + infix fun MutableList.unwind( + field: KProperty<*> + ): MutableList { + add(Aggregation.unwind(field.mongoName)) + return this + } + + @JvmName("unwindString") + infix fun MutableList.unwind( + field: () -> String + ): MutableList { + add(Aggregation.unwind(field())) + return this + } + + infix fun MutableList.unwind( + field: () -> KProperty<*> + ): MutableList { + add(Aggregation.unwind(field().mongoName)) + return this + } + + infix fun MutableList.sort( + builder: SortBuilder.() -> Unit + ): MutableList { + val sortBuilder = SortBuilder() + sortBuilder.invoke(builder) + add(sortBuilder.sortOperation) + return this + } + + infix fun MutableList.skip( + skip: Long + ): MutableList { + add(Aggregation.skip(skip)) + return this + } + + infix fun MutableList.limit( + limit: Long + ): MutableList { + add(Aggregation.limit(limit)) + return this + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonConverter.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonConverter.kt new file mode 100644 index 0000000..6e6f7a0 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonConverter.kt @@ -0,0 +1,33 @@ +package cn.tursom.database.mongodb.spring + +import org.bson.types.Decimal128 +import java.math.BigDecimal + +/** + * 用来将Object类型转换为Document类型的转换器 + * 设计成接口与单例对象共存的方式,即 + * 既可以用 BsonConverter.bsonValue + * 也可以让类继承于 BsonConverter 以直接调用 Any.bsonValue 方法 + */ +interface BsonConverter { + companion object { + fun bsonValue(obj: Any): Any = when (obj) { + is BigDecimal -> Decimal128(obj) + is String, is Number, is Boolean -> obj + is Map<*, *> -> MongoUtil.convertToBson(obj) + is Iterable<*> -> obj.mapNotNull { bsonValue(it ?: return@mapNotNull null) } + is Array<*> -> obj.mapNotNull { bsonValue(it ?: return@mapNotNull null) } + is ByteArray -> obj + is ShortArray -> obj.asList() + is IntArray -> obj.asList() + is LongArray -> obj.asList() + is FloatArray -> obj.asList() + is DoubleArray -> obj.asList() + is BooleanArray -> obj.asList() + is CharArray -> obj.asList() + else -> MongoUtil.convertToBson(obj) + } + } + + fun Any.bsonValue(): Any = bsonValue(this) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactory.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactory.kt new file mode 100644 index 0000000..52bdaf9 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactory.kt @@ -0,0 +1,24 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.core.Parser +import org.bson.Document + +interface BsonFactory { + val clazz: Class + + fun parse(document: Document) = Parser.parse(document, clazz)!! + + fun convertToBson(entity: T): Document + + //fun convertToEntity(bson: Document): T + + companion object { + operator fun get(clazz: Class): BsonFactory { + return BsonFactoryImpl(clazz) + } + + inline fun get(): BsonFactory { + return get(T::class.java) + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactoryImpl.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactoryImpl.kt new file mode 100644 index 0000000..9436deb --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/BsonFactoryImpl.kt @@ -0,0 +1,24 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.core.isStatic +import cn.tursom.core.isTransient +import org.bson.Document + +open class BsonFactoryImpl(final override val clazz: Class) : BsonFactory { + private val fields = clazz.declaredFields.filter { + it.isAccessible = true + !it.isStatic() && !it.isTransient() + } + + override fun convertToBson(entity: T): Document { + val bson = Document() + fields.forEach { + MongoUtil.injectValue(bson, it.get(entity) ?: return@forEach, it) + } + return bson + } + + override fun toString(): String { + return "BsonFactoryImpl(clazz=$clazz, fields=$fields)" + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaBuilder.kt new file mode 100644 index 0000000..b125d3b --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaBuilder.kt @@ -0,0 +1,155 @@ +package cn.tursom.database.mongodb.spring + +import org.bson.BsonRegularExpression +import org.bson.Document +import org.bson.types.ObjectId +import org.springframework.data.domain.Example +import org.springframework.data.geo.Circle +import org.springframework.data.geo.Point +import org.springframework.data.geo.Shape +import org.springframework.data.mongodb.core.geo.GeoJson +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.schema.MongoJsonSchema +import java.util.regex.Pattern +import kotlin.reflect.KProperty + +@Suppress("MemberVisibilityCanBePrivate", "unused", "HasPlatformType") +object CriteriaBuilder : MongoName, BsonConverter { + const val where = "\$where" + fun String.asJs() = where equal this + fun String.asObjectId() = ObjectId(this) + + infix fun where(where: String) = Criteria.where(where) + infix fun where(where: KProperty<*>) = Criteria.where(where.mongoName) + fun String.asWhere() = Criteria.where(this) + fun KProperty<*>.asWhere() = mongoName.asWhere() + + infix fun Criteria.and(where: Criteria) = Criteria().andOperator(this, where) + infix operator fun Criteria.plus(where: Criteria) = and(where) + + private val orOperator: Criteria.(Array) -> Criteria = Criteria::orOperator + private val andOperator: Criteria.(Array) -> Criteria = Criteria::andOperator + + fun or(vararg where: Criteria) = Criteria().orOperator(where) + val or: (Array) -> Criteria = CriteriaBuilder::or + fun and(vararg where: Criteria) = Criteria().andOperator(where) + val and: (Array) -> Criteria = CriteriaBuilder::and + + infix fun Criteria.or(where: Criteria) = or(this, where) + + infix fun Criteria.gt(where: Any) = gt(where.bsonValue()) // > + infix fun Criteria.gte(where: Any) = gte(where.bsonValue()) // >= + + infix fun Criteria.`in`(where: Collection) = `in`(where.mapNotNull { it?.bsonValue() }) // contains + infix fun Criteria.`is`(where: Any?) = `is`(where?.bsonValue()) // == + infix fun Criteria.ne(where: Any?) = ne(where?.bsonValue()) // != + infix fun Criteria.lt(where: Any) = lt(where.bsonValue()) // < + infix fun Criteria.lte(where: Any) = lte(where.bsonValue()) // <= + infix fun Criteria.nin(where: Collection) = nin(where.bsonValue()) // not contains + + infix fun Criteria.all(where: Any?) = all(where?.bsonValue()) + infix fun Criteria.all(where: Collection) = all(where.bsonValue()) + infix fun Criteria.size(s: Int) = size(s) + infix fun Criteria.exists(b: Boolean) = exists(b) + infix fun Criteria.type(t: Int) = type(t) + operator fun Criteria.not() = not() + infix fun Criteria.regex(re: String) = regex(re) + infix fun Criteria.regex(pattern: Pattern) = regex(pattern) + infix fun Criteria.regex(pattern: Regex) = regex(pattern.toPattern()) + infix fun Criteria.regex(regex: BsonRegularExpression) = regex(regex) + infix fun Criteria.withinSphere(circle: Circle) = withinSphere(circle) + infix fun Criteria.within(shape: Shape) = within(shape) + infix fun Criteria.near(point: Point) = near(point) + infix fun Criteria.nearSphere(point: Point) = nearSphere(point) + infix fun Criteria.intersects(point: GeoJson<*>) = intersects(point) + infix fun Criteria.maxDistance(maxDistance: Double) = maxDistance(maxDistance) + infix fun Criteria.minDistance(maxDistance: Double) = minDistance(maxDistance) + infix fun Criteria.elemMatch(c: Criteria) = elemMatch(c) + infix fun Criteria.alike(sample: Example<*>) = alike(sample) + infix fun Criteria.andDocumentStructureMatches(schema: MongoJsonSchema) = andDocumentStructureMatches(schema) + + infix fun Criteria.eq(where: Any?) = `is`(where?.bsonValue()) + infix fun Criteria.equal(where: Any?) = `is`(where?.bsonValue()) + + infix fun KProperty<*>.gt(where: Any) = asWhere() gt where // > + infix fun KProperty<*>.gte(where: Any) = asWhere() gte where // >= + fun KProperty<*>.`in`(vararg where: Any?) = asWhere() `in` where.asList() // contains + infix fun KProperty<*>.`in`(where: Collection) = asWhere() `in` where // contains + infix fun KProperty<*>.`is`(where: Any?) = asWhere() `is` where // == + infix fun KProperty<*>.ne(where: Any?) = asWhere() ne where // != + infix fun KProperty<*>.lt(where: Any) = asWhere() lt where // < + infix fun KProperty<*>.lte(where: Any) = asWhere() lte where // <= + fun KProperty<*>.nin(vararg where: Any?) = asWhere() nin where.asList() // not contains + infix fun KProperty<*>.nin(where: Collection) = asWhere() nin where // not contains + + infix fun KProperty<*>.all(where: Any?) = asWhere() all where + infix fun KProperty<*>.all(where: Collection) = asWhere() all where + infix fun KProperty<*>.size(s: Int) = asWhere() size s + infix fun KProperty<*>.exists(b: Boolean) = asWhere() exists b + infix fun KProperty<*>.type(t: Int) = asWhere() type t + operator fun KProperty<*>.not() = asWhere().not() + infix fun KProperty<*>.regex(re: String) = asWhere() regex re + infix fun KProperty<*>.regex(pattern: Pattern) = asWhere() regex pattern + infix fun KProperty<*>.regex(pattern: Regex) = asWhere() regex pattern.toPattern() + infix fun KProperty<*>.regex(regex: BsonRegularExpression) = asWhere() regex regex + infix fun KProperty<*>.withinSphere(circle: Circle) = asWhere() withinSphere circle + infix fun KProperty<*>.within(shape: Shape) = asWhere() within shape + infix fun KProperty<*>.near(point: Point) = asWhere() near point + infix fun KProperty<*>.nearSphere(point: Point) = asWhere() nearSphere point + infix fun KProperty<*>.intersects(point: GeoJson<*>) = asWhere() intersects point + infix fun KProperty<*>.maxDistance(maxDistance: Double) = asWhere() maxDistance maxDistance + infix fun KProperty<*>.minDistance(maxDistance: Double) = asWhere() minDistance maxDistance + infix fun KProperty<*>.elemMatch(c: Criteria) = asWhere() elemMatch c + infix fun KProperty<*>.alike(sample: Example<*>) = asWhere() alike sample + infix fun KProperty<*>.andDocumentStructureMatches(schema: MongoJsonSchema) = + asWhere() andDocumentStructureMatches schema + + infix fun KProperty<*>.eq(where: Any?) = asWhere() equal where + infix fun KProperty<*>.equal(where: Any?) = asWhere() equal where + + infix fun String.gt(where: Any) = asWhere() gt where // > + infix fun String.gte(where: Any) = asWhere() gte where // >= + fun String.`in`(vararg where: Any?) = asWhere() `in` where.asList() // contains + infix fun String.`in`(where: Collection) = asWhere() `in` where // contains + infix fun String.`is`(where: Any?) = asWhere() `is` where // == + infix fun String.ne(where: Any?) = asWhere() ne where // != + infix fun String.lt(where: Any) = asWhere() lt where // < + infix fun String.lte(where: Any) = asWhere() lte where // <= + fun String.nin(vararg where: Any?) = asWhere() nin where.asList() // not contains + infix fun String.nin(where: Collection) = asWhere() nin where // not contains + + infix fun String.all(where: Any?) = asWhere() all where + infix fun String.all(where: Collection) = asWhere() all where + infix fun String.size(s: Int) = asWhere() size s + infix fun String.exists(b: Boolean) = asWhere() exists b + infix fun String.type(t: Int) = asWhere() type t + operator fun String.not() = asWhere().not() + infix fun String.regex(re: String) = asWhere() regex re + infix fun String.regex(pattern: Pattern) = asWhere() regex pattern + infix fun String.regex(pattern: Regex) = asWhere() regex pattern.toPattern() + infix fun String.regex(regex: BsonRegularExpression) = asWhere() regex regex + infix fun String.withinSphere(circle: Circle) = asWhere() withinSphere circle + infix fun String.within(shape: Shape) = asWhere() within shape + infix fun String.near(point: Point) = asWhere() near point + infix fun String.nearSphere(point: Point) = asWhere() nearSphere point + infix fun String.intersects(point: GeoJson<*>) = asWhere() intersects point + infix fun String.maxDistance(maxDistance: Double) = asWhere() maxDistance maxDistance + infix fun String.minDistance(maxDistance: Double) = asWhere() minDistance maxDistance + infix fun String.elemMatch(c: Criteria) = asWhere() elemMatch c + infix fun String.alike(sample: Example<*>) = asWhere() alike sample + infix fun String.andDocumentStructureMatches(schema: MongoJsonSchema) = asWhere() andDocumentStructureMatches schema + + infix fun String.eq(where: Any?) = asWhere() equal where + infix fun String.equal(where: Any?) = asWhere() equal where + + inline infix operator fun invoke(operator: CriteriaBuilder.() -> Criteria): Criteria = this.operator() + inline infix fun query(operator: CriteriaBuilder.() -> Criteria): Query = Query(this.operator()) + inline infix fun queryObject(operator: CriteriaBuilder.() -> Criteria): Document = + Query(this.operator()).queryObject + + inline infix fun fieldsObject(operator: CriteriaBuilder.() -> Criteria): Document = + Query(this.operator()).fieldsObject + + inline infix fun sortObject(operator: CriteriaBuilder.() -> Criteria): Document = Query(this.operator()).sortObject +} diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaQuery.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaQuery.kt new file mode 100644 index 0000000..62c31b3 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/CriteriaQuery.kt @@ -0,0 +1,8 @@ +package cn.tursom.database.mongodb.spring + +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query + +object CriteriaQuery : MongoName, BsonConverter { + inline infix operator fun invoke(operator: CriteriaBuilder.() -> Criteria): Query = Query(CriteriaBuilder.operator()) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/GroupBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/GroupBuilder.kt new file mode 100644 index 0000000..1348dc0 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/GroupBuilder.kt @@ -0,0 +1,57 @@ +package cn.tursom.database.mongodb.spring + +import org.springframework.data.mongodb.core.aggregation.Aggregation +import org.springframework.data.mongodb.core.aggregation.GroupOperation +import kotlin.reflect.KProperty + +@Suppress("unused") +object GroupBuilder : MongoName, BsonConverter { + inline infix operator fun invoke(operator: GroupBuilder.() -> GroupOperation): GroupOperation = this.operator() + + fun group(vararg fields: String): GroupOperation = Aggregation.group(*fields) + fun group(vararg fields: KProperty<*>): GroupOperation = + Aggregation.group(*fields.map { it.mongoName }.toTypedArray()) + + infix fun GroupOperation.sum(reference: String): GroupOperation.GroupOperationBuilder = sum(reference) + infix fun GroupOperation.sum(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = sum(reference.mongoName) + + infix fun GroupOperation.GroupOperationBuilder.`as`(alias: String): GroupOperation = `as`(alias) + infix fun GroupOperation.GroupOperationBuilder.`as`(alias: KProperty<*>): GroupOperation = `as`(alias.mongoName) + + infix fun GroupOperation.addToSet(reference: String): GroupOperation.GroupOperationBuilder = addToSet(reference) + infix fun GroupOperation.addToSet(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + addToSet(reference.mongoName) + + infix fun GroupOperation.addToSet(value: Any): GroupOperation.GroupOperationBuilder = addToSet(value) + + infix fun GroupOperation.last(reference: String): GroupOperation.GroupOperationBuilder = last(reference) + infix fun GroupOperation.last(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + last(reference.mongoName) + + infix fun GroupOperation.first(reference: String): GroupOperation.GroupOperationBuilder = first(reference) + infix fun GroupOperation.first(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + first(reference.mongoName) + + infix fun GroupOperation.avg(reference: String): GroupOperation.GroupOperationBuilder = avg(reference) + infix fun GroupOperation.avg(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = avg(reference.mongoName) + + infix fun GroupOperation.push(reference: String): GroupOperation.GroupOperationBuilder = push(reference) + infix fun GroupOperation.push(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + push(reference.mongoName) + + infix fun GroupOperation.push(value: Any): GroupOperation.GroupOperationBuilder = push(value) + + infix fun GroupOperation.min(reference: String): GroupOperation.GroupOperationBuilder = min(reference) + infix fun GroupOperation.min(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = min(reference.mongoName) + + infix fun GroupOperation.max(reference: String): GroupOperation.GroupOperationBuilder = max(reference) + infix fun GroupOperation.max(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = max(reference.mongoName) + + infix fun GroupOperation.stdDevSamp(reference: String): GroupOperation.GroupOperationBuilder = stdDevSamp(reference) + infix fun GroupOperation.stdDevSamp(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + stdDevSamp(reference.mongoName) + + infix fun GroupOperation.stdDevPop(reference: String): GroupOperation.GroupOperationBuilder = stdDevPop(reference) + infix fun GroupOperation.stdDevPop(reference: KProperty<*>): GroupOperation.GroupOperationBuilder = + stdDevPop(reference.mongoName) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoName.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoName.kt new file mode 100644 index 0000000..e8c4522 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoName.kt @@ -0,0 +1,87 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.database.mongodb.spring.function.Function1 +import cn.tursom.database.mongodb.spring.function.field +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.Field +import kotlin.reflect.KProperty +import kotlin.reflect.jvm.javaField + + +interface MongoName { + companion object { + const val dollar = '$' + inline operator fun invoke(builder: Companion.() -> T) = this.builder() + + inline val KProperty<*>.mongoName: String + get() = mongoName(this) + + fun arrayElement(elementName: String = "") = if (elementName.isBlank()) { + "$" + } else { + "$[$elementName]" + } + + fun mongoName(kProperty: KProperty<*>): String { + if (kProperty.javaField?.getAnnotation(Id::class.java) != null) { + return "_id" + } + val field = kProperty.javaField?.getAnnotation(Field::class.java) + if (field != null) { + if (field.value.isNotEmpty()) { + return field.value + } + } + return kProperty.name + } + + fun mongoName(field: java.lang.reflect.Field): String { + if (field.getAnnotation(Id::class.java) != null) { + return "_id" + } + val fieldAnnotation = field.getAnnotation(Field::class.java) + if (fieldAnnotation != null) { + if (fieldAnnotation.value.isNotEmpty()) { + return fieldAnnotation.value + } + } + return field.name + } + + fun mongoName(getter: Function1<*, T>): String = mongoName(getter.field!!) + + infix operator fun KProperty<*>.plus(kProperty: KProperty<*>) = "$mongoName.${kProperty.mongoName}" + infix operator fun KProperty<*>.plus(field: String) = "$mongoName.${field}" + infix operator fun String.plus(kProperty: KProperty<*>) = "$this.${kProperty.mongoName}" + infix operator fun String.plus(field: String) = "$this.${field}" + + infix operator fun KProperty<*>.get(kProperty: KProperty<*>) = plus(kProperty) + infix operator fun KProperty<*>.get(field: String) = plus(field) + infix operator fun String.get(kProperty: KProperty<*>) = plus(kProperty) + infix operator fun String.get(field: String) = plus(field) + + infix operator fun KProperty<*>.rem(kProperty: KProperty<*>) = plus(kProperty) + infix operator fun KProperty<*>.rem(field: String) = plus(field) + infix operator fun String.rem(kProperty: KProperty<*>) = plus(kProperty) + infix operator fun String.rem(field: String) = plus(field) + } + + val dollar get() = Companion.dollar + val KProperty<*>.mongoName: String + get() = mongoName(this) + + fun arrayElement(elementName: String = "") = Companion.arrayElement(elementName) + fun mongoName(kProperty: KProperty<*>): String = Companion.mongoName(kProperty) + fun mongoName(field: java.lang.reflect.Field): String = Companion.mongoName(field) + fun mongoName(getter: Function1<*, T>): String = Companion.mongoName(getter) + + infix operator fun KProperty<*>.get(kProperty: KProperty<*>) = Companion { this@get plus kProperty } + infix operator fun KProperty<*>.get(field: String) = Companion { this@get plus field } + infix operator fun String.get(kProperty: KProperty<*>) = Companion { this@get plus kProperty } + infix operator fun String.get(field: String) = Companion { this@get plus field } + + infix operator fun KProperty<*>.rem(kProperty: KProperty<*>) = Companion { this@rem plus kProperty } + infix operator fun KProperty<*>.rem(field: String) = Companion { this@rem plus field } + infix operator fun String.rem(kProperty: KProperty<*>) = Companion { this@rem plus kProperty } + infix operator fun String.rem(field: String) = Companion { this@rem plus field } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoUtil.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoUtil.kt new file mode 100644 index 0000000..137a9e2 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/MongoUtil.kt @@ -0,0 +1,82 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.core.isStatic +import cn.tursom.core.isTransient +import cn.tursom.core.uncheckedCast +import org.bson.BsonValue +import org.bson.Document +import org.bson.conversions.Bson +import java.lang.reflect.Field + +object MongoUtil { + fun collectionName(clazz: Class<*>) = + clazz.getAnnotation(org.springframework.data.mongodb.core.mapping.Document::class.java)?.let { + when { + it.value.isNotBlank() -> it.value + it.collection.isNotBlank() -> it.collection + else -> null + } + } ?: clazz.simpleName.toCharArray().let { + it[0] = it[0].toLowerCase() + it + }.concatToString() + + fun convertToBson(entity: Any): Document { + return when (entity) { + is Document -> entity + is Map<*, *> -> entity.convert() + else -> { + val bson = Document() + entity.javaClass.declaredFields.filter { + it.isAccessible = true + !it.isStatic() + && !it.isTransient() + //&& it.getAnnotation(Ignore::class.java) == null + && (it.type != Lazy::class.java || it.get(entity).uncheckedCast>().isInitialized()) + }.forEach { + injectValue(bson, it.get(entity) ?: return@forEach, it) + } + bson + } + } + } + + fun injectValue(bson: Document, value: Any, field: Field) { + when (value) { + is Pair<*, *> -> bson[value.first?.toString() ?: return] = convertToBson(value.second ?: return) + is Map.Entry<*, *> -> bson[value.key?.toString() ?: return] = + convertToBson(value.value ?: return) + else -> bson[MongoName.mongoName(field)] = value.convert() ?: return + } + } + + fun fromBson(document: Document, bsonFactory: BsonFactory) = bsonFactory.parse(document) + inline fun fromBson(document: Document) = BsonFactory[T::class.java].parse(document) + + private fun Iterator<*>.convert(): List<*> { + val list = ArrayList() + forEach { + list.add(it.convert() ?: return@forEach) + } + return list + } + + private fun Map<*, *>.convert(): Document { + val doc = Document() + forEach { any, u -> + any ?: return@forEach + doc[any.toString()] = u.convert() ?: return@forEach + } + return doc + } + + private fun Any?.convert() = when (this) { + null -> null + is Enum<*> -> name + is Boolean, is Number, is String, is Bson, is BsonValue -> this + is Map<*, *> -> this.convert() + is Iterator<*> -> this.convert() + is Iterable<*> -> this.iterator().convert() + else -> convertToBson(this) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/ProjectBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/ProjectBuilder.kt new file mode 100644 index 0000000..8cb0eff --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/ProjectBuilder.kt @@ -0,0 +1,324 @@ +package cn.tursom.database.mongodb.spring + +import org.springframework.data.mongodb.core.aggregation.Aggregation +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators +import org.springframework.data.mongodb.core.aggregation.Fields +import org.springframework.data.mongodb.core.aggregation.ProjectionOperation +import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder +import kotlin.reflect.KProperty + +@Suppress("unused", "MemberVisibilityCanBePrivate") +object ProjectBuilder : MongoName, BsonConverter { + inline infix operator fun invoke( + operator: ProjectBuilder.() -> ProjectionOperation + ): ProjectionOperation = this.operator() + + fun project(): ProjectionOperation = Aggregation.project() + fun project(vararg fields: String): ProjectionOperation = Aggregation.project(*fields) + fun project(vararg fields: KProperty<*>): ProjectionOperation = + Aggregation.project(*fields.map { it.mongoName }.toTypedArray()) + + infix fun ProjectionOperation.and(name: String): ProjectionOperationBuilder = and(name) + infix fun ProjectionOperation.and(name: KProperty<*>): ProjectionOperationBuilder = and(name.mongoName) + + infix fun ProjectionOperation.andExclude(fields: KProperty<*>): ProjectionOperation = andExclude(listOf(fields)) + fun ProjectionOperation.andExclude(vararg fields: KProperty<*>): ProjectionOperation = andExclude(fields.asList()) + infix fun ProjectionOperation.andExclude(fields: Collection>): ProjectionOperation = + andExclude(*fields.map { it.mongoName }.toTypedArray()) + + infix fun ProjectionOperation.andInclude(fields: KProperty<*>): ProjectionOperation = andInclude(listOf(fields)) + fun ProjectionOperation.andInclude(vararg fields: KProperty<*>): ProjectionOperation = andInclude(fields.asList()) + infix fun ProjectionOperation.andInclude(fields: Collection>): ProjectionOperation = + andInclude(*fields.map { it.mongoName }.toTypedArray()) + + + infix fun ProjectionOperationBuilder.nested(fields: Fields): ProjectionOperation = nested(fields) + infix fun ProjectionOperationBuilder.`as`(alias: String): ProjectionOperation = `as`(alias) + infix fun ProjectionOperationBuilder.alias(alias: String): ProjectionOperation = `as`(alias) + infix fun ProjectionOperationBuilder.`as`(alias: KProperty<*>): ProjectionOperation = `as`(alias.mongoName) + infix fun ProjectionOperationBuilder.alias(alias: KProperty<*>): ProjectionOperation = `as`(alias.mongoName) + infix fun ProjectionOperationBuilder.applyCondition(cond: ConditionalOperators.Cond): ProjectionOperation = + applyCondition(cond) + + infix fun ProjectionOperationBuilder.applyCondition(ifNull: ConditionalOperators.IfNull): ProjectionOperation = + applyCondition(ifNull) + + infix operator fun ProjectionOperationBuilder.plus(number: Number): ProjectionOperationBuilder = plus(number) + infix operator fun ProjectionOperationBuilder.plus(fieldReference: String): ProjectionOperationBuilder = + plus(fieldReference) + + infix operator fun ProjectionOperationBuilder.plus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + plus(fieldReference.mongoName) + + infix operator fun ProjectionOperationBuilder.minus(number: Number): ProjectionOperationBuilder = minus(number) + infix operator fun ProjectionOperationBuilder.minus(fieldReference: String): ProjectionOperationBuilder = + minus(fieldReference) + + infix operator fun ProjectionOperationBuilder.minus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + minus(fieldReference.mongoName) + + infix fun ProjectionOperationBuilder.multiply(number: Number): ProjectionOperationBuilder = multiply(number) + infix fun ProjectionOperationBuilder.multiply(fieldReference: String): ProjectionOperationBuilder = + multiply(fieldReference) + + infix fun ProjectionOperationBuilder.multiply(fieldReference: KProperty<*>): ProjectionOperationBuilder = + multiply(fieldReference.mongoName) + + infix operator fun ProjectionOperationBuilder.times(number: Number): ProjectionOperationBuilder = multiply(number) + infix operator fun ProjectionOperationBuilder.times(fieldReference: String): ProjectionOperationBuilder = + multiply(fieldReference) + + infix operator fun ProjectionOperationBuilder.times(fieldReference: KProperty<*>): ProjectionOperationBuilder = + multiply(fieldReference.mongoName) + + infix fun ProjectionOperationBuilder.divide(number: Number): ProjectionOperationBuilder = divide(number) + infix fun ProjectionOperationBuilder.divide(fieldReference: String): ProjectionOperationBuilder = + divide(fieldReference) + + infix fun ProjectionOperationBuilder.divide(fieldReference: KProperty<*>): ProjectionOperationBuilder = + divide(fieldReference.mongoName) + + infix operator fun ProjectionOperationBuilder.div(number: Number): ProjectionOperationBuilder = divide(number) + infix operator fun ProjectionOperationBuilder.div(fieldReference: String): ProjectionOperationBuilder = + divide(fieldReference) + + infix operator fun ProjectionOperationBuilder.div(fieldReference: KProperty<*>): ProjectionOperationBuilder = + divide(fieldReference.mongoName) + + infix fun ProjectionOperationBuilder.mod(number: Number): ProjectionOperationBuilder = mod(number) + infix fun ProjectionOperationBuilder.mod(fieldReference: String): ProjectionOperationBuilder = mod(fieldReference) + infix fun ProjectionOperationBuilder.mod(fieldReference: KProperty<*>): ProjectionOperationBuilder = + mod(fieldReference.mongoName) + + infix fun ProjectionOperationBuilder.cmp(compareValue: Any): ProjectionOperationBuilder = cmp(compareValue) + infix fun ProjectionOperationBuilder.eq(compareValue: Any): ProjectionOperationBuilder = eq(compareValue) + infix fun ProjectionOperationBuilder.gt(compareValue: Any): ProjectionOperationBuilder = gt(compareValue) + infix fun ProjectionOperationBuilder.gte(compareValue: Any): ProjectionOperationBuilder = gte(compareValue) + infix fun ProjectionOperationBuilder.lt(compareValue: Any): ProjectionOperationBuilder = lt(compareValue) + infix fun ProjectionOperationBuilder.lte(compareValue: Any): ProjectionOperationBuilder = lte(compareValue) + infix fun ProjectionOperationBuilder.ne(compareValue: Any): ProjectionOperationBuilder = ne(compareValue) + + infix fun ProjectionOperationBuilder.slice(count: Int): ProjectionOperationBuilder = slice(count) + + infix fun ProjectionOperationBuilder.log(baseFieldRef: String): ProjectionOperationBuilder = log(baseFieldRef) + infix fun ProjectionOperationBuilder.log(baseFieldRef: KProperty<*>): ProjectionOperationBuilder = + log(baseFieldRef.mongoName) + + infix fun ProjectionOperationBuilder.log(base: Number): ProjectionOperationBuilder = log(base) + + infix fun ProjectionOperationBuilder.pow(exponentFieldRef: String): ProjectionOperationBuilder = pow(exponentFieldRef) + infix fun ProjectionOperationBuilder.pow(exponentFieldRef: KProperty<*>): ProjectionOperationBuilder = + pow(exponentFieldRef.mongoName) + + infix fun ProjectionOperationBuilder.pow(exponent: Number): ProjectionOperationBuilder = pow(exponent) + + infix fun ProjectionOperationBuilder.strCaseCmpValueOf(fieldRef: String): ProjectionOperationBuilder = + strCaseCmpValueOf(fieldRef) + + infix fun ProjectionOperationBuilder.strCaseCmpValueOf(fieldRef: KProperty<*>): ProjectionOperationBuilder = + strCaseCmpValueOf(fieldRef.mongoName) + + @JvmName("concatArrays_") + fun ProjectionOperationBuilder.concatArrays(fields: Collection): ProjectionOperationBuilder = + concatArrays(*fields.toTypedArray()) + + fun ProjectionOperationBuilder.concatArrays(vararg fields: KProperty<*>): ProjectionOperationBuilder = + concatArrays(*fields.map { it.mongoName }.toTypedArray()) + + fun ProjectionOperationBuilder.concatArrays(fields: Collection>): ProjectionOperationBuilder = + concatArrays(*fields.map { it.mongoName }.toTypedArray()) + + + infix fun String.nested(fields: Fields): ProjectionOperation = project() and this nested (fields) + infix fun String.`as`(alias: String): ProjectionOperation = project() and this `as` (alias) + infix fun String.alias(alias: String): ProjectionOperation = project() and this `as` (alias) + infix fun String.`as`(alias: KProperty<*>): ProjectionOperation = project() and this `as` (alias.mongoName) + infix fun String.alias(alias: KProperty<*>): ProjectionOperation = project() and this `as` (alias.mongoName) + infix fun String.applyCondition(cond: ConditionalOperators.Cond): ProjectionOperation = + project() and this applyCondition (cond) + + infix fun String.applyCondition(ifNull: ConditionalOperators.IfNull): ProjectionOperation = + project() and this applyCondition (ifNull) + + infix operator fun String.plus(number: Number): ProjectionOperationBuilder = project() and this plus (number) + infix operator fun String.plus(fieldReference: String): ProjectionOperationBuilder = + project() and this plus (fieldReference) + + infix operator fun String.plus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this plus (fieldReference.mongoName) + + infix operator fun String.minus(number: Number): ProjectionOperationBuilder = project() and this minus (number) + infix operator fun String.minus(fieldReference: String): ProjectionOperationBuilder = + project() and this minus (fieldReference) + + infix operator fun String.minus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this minus (fieldReference.mongoName) + + infix fun String.multiply(number: Number): ProjectionOperationBuilder = project() and this multiply (number) + infix fun String.multiply(fieldReference: String): ProjectionOperationBuilder = + project() and this multiply (fieldReference) + + infix fun String.multiply(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this multiply (fieldReference.mongoName) + + infix operator fun String.times(number: Number): ProjectionOperationBuilder = project() and this multiply (number) + infix operator fun String.times(fieldReference: String): ProjectionOperationBuilder = + project() and this multiply (fieldReference) + + infix operator fun String.times(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this multiply (fieldReference.mongoName) + + infix fun String.divide(number: Number): ProjectionOperationBuilder = project() and this divide (number) + infix fun String.divide(fieldReference: String): ProjectionOperationBuilder = + project() and this divide (fieldReference) + + infix fun String.divide(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this divide (fieldReference.mongoName) + + infix operator fun String.div(number: Number): ProjectionOperationBuilder = project() and this divide (number) + infix operator fun String.div(fieldReference: String): ProjectionOperationBuilder = + project() and this divide (fieldReference) + + infix operator fun String.div(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this divide (fieldReference.mongoName) + + infix fun String.mod(number: Number): ProjectionOperationBuilder = project() and this mod (number) + infix fun String.mod(fieldReference: String): ProjectionOperationBuilder = project() and this mod (fieldReference) + infix fun String.mod(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this mod (fieldReference.mongoName) + + infix fun String.cmp(compareValue: Any): ProjectionOperationBuilder = project() and this cmp (compareValue) + infix fun String.eq(compareValue: Any): ProjectionOperationBuilder = project() and this eq (compareValue) + infix fun String.gt(compareValue: Any): ProjectionOperationBuilder = project() and this gt (compareValue) + infix fun String.gte(compareValue: Any): ProjectionOperationBuilder = project() and this gte (compareValue) + infix fun String.lt(compareValue: Any): ProjectionOperationBuilder = project() and this lt (compareValue) + infix fun String.lte(compareValue: Any): ProjectionOperationBuilder = project() and this lte (compareValue) + infix fun String.ne(compareValue: Any): ProjectionOperationBuilder = project() and this ne (compareValue) + + infix fun String.slice(count: Int): ProjectionOperationBuilder = project() and this slice (count) + fun String.slice(count: Int, offset: Int): ProjectionOperationBuilder = (project() and this).slice(count, offset) + + infix fun String.log(baseFieldRef: String): ProjectionOperationBuilder = project() and this log (baseFieldRef) + infix fun String.log(baseFieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this log (baseFieldRef.mongoName) + + infix fun String.log(base: Number): ProjectionOperationBuilder = project() and this log (base) + + infix fun String.pow(exponentFieldRef: String): ProjectionOperationBuilder = project() and this pow (exponentFieldRef) + infix fun String.pow(exponentFieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this pow (exponentFieldRef.mongoName) + + infix fun String.pow(exponent: Number): ProjectionOperationBuilder = project() and this pow (exponent) + + infix fun String.strCaseCmpValueOf(fieldRef: String): ProjectionOperationBuilder = + project() and this strCaseCmpValueOf (fieldRef) + + infix fun String.strCaseCmpValueOf(fieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this strCaseCmpValueOf (fieldRef.mongoName) + + + infix fun KProperty<*>.nested(fields: Fields): ProjectionOperation = project() and this nested (fields) + infix fun KProperty<*>.`as`(alias: String): ProjectionOperation = project() and this `as` (alias) + infix fun KProperty<*>.alias(alias: String): ProjectionOperation = project() and this `as` (alias) + infix fun KProperty<*>.`as`(alias: KProperty<*>): ProjectionOperation = project() and this `as` (alias.mongoName) + infix fun KProperty<*>.alias(alias: KProperty<*>): ProjectionOperation = project() and this `as` (alias.mongoName) + infix fun KProperty<*>.applyCondition(cond: ConditionalOperators.Cond): ProjectionOperation = + project() and this applyCondition (cond) + + infix fun KProperty<*>.applyCondition(ifNull: ConditionalOperators.IfNull): ProjectionOperation = + project() and this applyCondition (ifNull) + + infix operator fun KProperty<*>.plus(number: Number): ProjectionOperationBuilder = project() and this plus (number) + infix operator fun KProperty<*>.plus(fieldReference: String): ProjectionOperationBuilder = + project() and this plus (fieldReference) + + infix operator fun KProperty<*>.plus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this plus (fieldReference.mongoName) + + infix operator fun KProperty<*>.minus(number: Number): ProjectionOperationBuilder = project() and this minus (number) + infix operator fun KProperty<*>.minus(fieldReference: String): ProjectionOperationBuilder = + project() and this minus (fieldReference) + + infix operator fun KProperty<*>.minus(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this minus (fieldReference.mongoName) + + infix fun KProperty<*>.multiply(number: Number): ProjectionOperationBuilder = project() and this multiply (number) + infix fun KProperty<*>.multiply(fieldReference: String): ProjectionOperationBuilder = + project() and this multiply (fieldReference) + + infix fun KProperty<*>.multiply(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this multiply (fieldReference.mongoName) + + infix operator fun KProperty<*>.times(number: Number): ProjectionOperationBuilder = + project() and this multiply (number) + + infix operator fun KProperty<*>.times(fieldReference: String): ProjectionOperationBuilder = + project() and this multiply (fieldReference) + + infix operator fun KProperty<*>.times(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this multiply (fieldReference.mongoName) + + infix fun KProperty<*>.divide(number: Number): ProjectionOperationBuilder = project() and this divide (number) + infix fun KProperty<*>.divide(fieldReference: String): ProjectionOperationBuilder = + project() and this divide (fieldReference) + + infix fun KProperty<*>.divide(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this divide (fieldReference.mongoName) + + infix operator fun KProperty<*>.div(number: Number): ProjectionOperationBuilder = project() and this divide (number) + infix operator fun KProperty<*>.div(fieldReference: String): ProjectionOperationBuilder = + project() and this divide (fieldReference) + + infix operator fun KProperty<*>.div(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this divide (fieldReference.mongoName) + + infix fun KProperty<*>.mod(number: Number): ProjectionOperationBuilder = project() and this mod (number) + infix fun KProperty<*>.mod(fieldReference: String): ProjectionOperationBuilder = + project() and this mod (fieldReference) + + infix fun KProperty<*>.mod(fieldReference: KProperty<*>): ProjectionOperationBuilder = + project() and this mod (fieldReference.mongoName) + + infix fun KProperty<*>.cmp(compareValue: Any): ProjectionOperationBuilder = project() and this cmp (compareValue) + infix fun KProperty<*>.eq(compareValue: Any): ProjectionOperationBuilder = project() and this eq (compareValue) + infix fun KProperty<*>.gt(compareValue: Any): ProjectionOperationBuilder = project() and this gt (compareValue) + infix fun KProperty<*>.gte(compareValue: Any): ProjectionOperationBuilder = project() and this gte (compareValue) + infix fun KProperty<*>.lt(compareValue: Any): ProjectionOperationBuilder = project() and this lt (compareValue) + infix fun KProperty<*>.lte(compareValue: Any): ProjectionOperationBuilder = project() and this lte (compareValue) + infix fun KProperty<*>.ne(compareValue: Any): ProjectionOperationBuilder = project() and this ne (compareValue) + + infix fun KProperty<*>.slice(count: Int): ProjectionOperationBuilder = project() and this slice (count) + fun KProperty<*>.slice(count: Int, offset: Int): ProjectionOperationBuilder = + (project() and this).slice(count, offset) + + infix fun KProperty<*>.log(baseFieldRef: String): ProjectionOperationBuilder = project() and this log (baseFieldRef) + infix fun KProperty<*>.log(baseFieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this log (baseFieldRef.mongoName) + + infix fun KProperty<*>.log(base: Number): ProjectionOperationBuilder = project() and this log (base) + + infix fun KProperty<*>.pow(exponentFieldRef: String): ProjectionOperationBuilder = + project() and this pow (exponentFieldRef) + + infix fun KProperty<*>.pow(exponentFieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this pow (exponentFieldRef.mongoName) + + infix fun KProperty<*>.pow(exponent: Number): ProjectionOperationBuilder = project() and this pow (exponent) + + infix fun KProperty<*>.strCaseCmpValueOf(fieldRef: String): ProjectionOperationBuilder = + project() and this strCaseCmpValueOf (fieldRef) + + infix fun KProperty<*>.strCaseCmpValueOf(fieldRef: KProperty<*>): ProjectionOperationBuilder = + project() and this strCaseCmpValueOf (fieldRef.mongoName) + + @JvmName("concatArrays_") + fun String.concatArrays(fields: Collection): ProjectionOperationBuilder = + (project() and this).concatArrays(*fields.toTypedArray()) + + fun String.concatArrays(vararg fields: KProperty<*>): ProjectionOperationBuilder = + (project() and this).concatArrays(*fields.map { it.mongoName }.toTypedArray()) + + fun String.concatArrays(fields: Collection>): ProjectionOperationBuilder = + (project() and this).concatArrays(*fields.map { it.mongoName }.toTypedArray()) + +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/QueryBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/QueryBuilder.kt new file mode 100644 index 0000000..1aa2abb --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/QueryBuilder.kt @@ -0,0 +1,331 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.core.uncheckedCast +import org.bson.BsonRegularExpression +import org.bson.Document +import org.bson.types.ObjectId +import org.springframework.data.domain.Example +import org.springframework.data.domain.Sort +import org.springframework.data.geo.Circle +import org.springframework.data.geo.Point +import org.springframework.data.geo.Shape +import org.springframework.data.mongodb.core.geo.GeoJson +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Field +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.schema.MongoJsonSchema +import java.util.* +import java.util.regex.Pattern +import kotlin.reflect.KProperty + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class QueryBuilder : MongoName { + companion object { + const val where = "\$where" + private val criteriaField: java.lang.reflect.Field? = try { + Criteria::class.java.getDeclaredField("criteria") + } catch (e: Exception) { + null + } + private val isValueField: java.lang.reflect.Field? = try { + Criteria::class.java.getDeclaredField("isValue") + } catch (e: Exception) { + null + } + private val criteriaChainField: java.lang.reflect.Field? = try { + Criteria::class.java.getDeclaredField("criteriaChain") + } catch (e: Exception) { + null + } + + init { + criteriaField?.isAccessible = true + isValueField?.isAccessible = true + criteriaChainField?.isAccessible = true + } + + inline infix operator fun invoke(operator: QueryBuilder.() -> Unit): Query = QueryBuilder().let { builder -> + builder.operator() + val query = Query(builder.and) + builder.queryHandler.forEach { handler -> + query.handler() + } + query + } + + infix fun queryObject(operator: QueryBuilder.() -> Unit): Document = this(operator).queryObject + infix fun fieldsObject(operator: QueryBuilder.() -> Unit): Document = this(operator).fieldsObject + infix fun sortObject(operator: QueryBuilder.() -> Unit): Document = this(operator).sortObject + + infix fun criteria(operator: QueryBuilder.() -> Unit): Criteria = QueryBuilder().apply(operator).and + } + + val queryHandler = LinkedList Unit>() + private var uniqueField = true + private val fieldSet = HashSet() + + val and: Criteria + get() = when (criteriaList.size) { + 0 -> Criteria() + 1 -> criteriaList.first() + else -> if (uniqueField && criteriaField != null && isValueField != null) { + var base = Criteria() + criteriaList.forEach { + if (it.key != null) { + base = base.and(it.key!!) + criteriaField.set(base, criteriaField.get(it)) + isValueField.set(base, isValueField.get(it)) + } else { + criteriaChainField?.get(base)?.uncheckedCast>() + ?.addAll(criteriaChainField.get(it).uncheckedCast()) + } + } + base + } else { + Criteria().andOperator(*criteriaList.toTypedArray()) + } + } + + val or: Criteria + get() = when (criteriaList.size) { + 0 -> Criteria() + 1 -> criteriaList.first() + else -> Criteria().orOperator(*criteriaList.toTypedArray()) + } + + private val criteriaList = ArrayList() + + fun query(handler: Query.() -> Unit) { + queryHandler.add(handler) + } + + fun String.asJs() = where equal this + fun String.asObjectId() = ObjectId(this) + + private fun String.asWhere() = Criteria.where(this).also { uniqueField = uniqueField && fieldSet.add(this) } + private fun KProperty<*>.asWhere() = + mongoName.asWhere().also { uniqueField = uniqueField && fieldSet.add(mongoName) } + + //infix fun Criteria.or(operator: QueryBuilder.() -> Unit) { + // criteriaList.add(QueryBuilder().also(operator).criteria) + //} + + fun or(operator: QueryBuilder.() -> Unit) { + QueryBuilder().also(operator).or.also(criteriaList::add) + } + + fun and(operator: QueryBuilder.() -> Unit) { + QueryBuilder().also(operator).and.also(criteriaList::add) + } + + infix fun KProperty<*>.gt(where: Any) = asWhere() gt where // > + infix fun KProperty<*>.gte(where: Any) = asWhere() gte where // >= + fun KProperty<*>.`in`(vararg where: Any?) = asWhere() `in` where // contains + infix fun KProperty<*>.`in`(where: Collection) = asWhere() `in` (where) // contains + infix fun KProperty<*>.`is`(where: Any?) = asWhere() `is` where // == + infix fun KProperty<*>.ne(where: Any?) = asWhere() ne where // != + infix fun KProperty<*>.lt(where: Any) = asWhere() lt where // < + infix fun KProperty<*>.lte(where: Any) = asWhere() lte where // <= + fun KProperty<*>.nin(vararg where: Any?) = asWhere() nin where // not contains + infix fun KProperty<*>.nin(where: Collection) = asWhere() nin where // not contains + + infix fun KProperty<*>.all(where: Any?) = asWhere() all where + infix fun KProperty<*>.all(where: Collection) = asWhere() all where + infix fun KProperty<*>.size(s: Int) = asWhere() size s + infix fun KProperty<*>.exists(b: Boolean) = asWhere() exists b + infix fun KProperty<*>.type(t: Int) = asWhere() type t + operator fun KProperty<*>.not() = asWhere().not().last(criteriaList::add) + infix fun KProperty<*>.regex(re: String) = asWhere() regex re + infix fun KProperty<*>.regex(pattern: Pattern) = asWhere() regex pattern + infix fun KProperty<*>.regex(pattern: Regex) = asWhere() regex pattern + infix fun KProperty<*>.regex(regex: BsonRegularExpression) = asWhere() regex regex + infix fun KProperty<*>.withinSphere(circle: Circle) = asWhere() withinSphere circle + infix fun KProperty<*>.within(shape: Shape) = asWhere() within shape + infix fun KProperty<*>.near(point: Point) = asWhere() near point + infix fun KProperty<*>.nearSphere(point: Point) = asWhere() nearSphere point + infix fun KProperty<*>.intersects(point: GeoJson<*>) = asWhere() intersects point + infix fun KProperty<*>.maxDistance(maxDistance: Double) = asWhere() maxDistance maxDistance + infix fun KProperty<*>.minDistance(maxDistance: Double) = asWhere() minDistance maxDistance + infix fun KProperty<*>.elemMatch(c: QueryBuilder.() -> Unit) = asWhere() elemMatch c + infix fun KProperty<*>.alike(sample: Example<*>) = asWhere() alike sample + infix fun KProperty<*>.andDocumentStructureMatches(schema: MongoJsonSchema) = + asWhere() andDocumentStructureMatches schema + + infix fun KProperty<*>.equal(where: Any?) = asWhere() equal where + infix fun KProperty<*>.eq(where: Any?) = asWhere() equal where + + infix fun KProperty<*>.equal(where: QueryBuilder.() -> Unit) = asWhere() equal QueryBuilder(where).queryObject + infix fun KProperty<*>.eq(where: QueryBuilder.() -> Unit) = asWhere() equal QueryBuilder(where).queryObject + + infix fun String.gt(where: Any) = asWhere() gt where // > + infix fun String.gte(where: Any) = asWhere() gte where // >= + fun String.`in`(vararg where: Any?) = asWhere() `in` where // contains + infix fun String.`in`(where: Collection) = asWhere() `in` where // contains + infix fun String.`is`(where: Any?) = asWhere() `is` where // == + infix fun String.ne(where: Any?) = asWhere() ne where // != + infix fun String.lt(where: Any) = asWhere() lt where // < + infix fun String.lte(where: Any) = asWhere() lte where // <= + fun String.nin(vararg where: Any?) = asWhere() nin where // not contains + infix fun String.nin(where: Collection) = asWhere() nin where // not contains + + infix fun String.all(where: Any?) = asWhere() all where + infix fun String.all(where: Collection) = asWhere() all where + infix fun String.size(s: Int) = asWhere() size s + infix fun String.exists(b: Boolean) = asWhere() exists b + infix fun String.type(t: Int) = asWhere() type t + operator fun String.not() = asWhere().not().last(criteriaList::add) + infix fun String.regex(re: String) = asWhere() regex re + infix fun String.regex(pattern: Pattern) = asWhere() regex pattern + infix fun String.regex(pattern: Regex) = asWhere() regex pattern.toPattern() + infix fun String.regex(regex: BsonRegularExpression) = asWhere() regex regex + infix fun String.withinSphere(circle: Circle) = asWhere() withinSphere circle + infix fun String.within(shape: Shape) = asWhere() within shape + infix fun String.near(point: Point) = asWhere() near point + infix fun String.nearSphere(point: Point) = asWhere() nearSphere point + infix fun String.intersects(point: GeoJson<*>) = asWhere() intersects point + infix fun String.maxDistance(maxDistance: Double) = asWhere() maxDistance maxDistance + infix fun String.minDistance(maxDistance: Double) = asWhere() minDistance maxDistance + infix fun String.elemMatch(c: QueryBuilder.() -> Unit) = asWhere() elemMatch c + infix fun String.alike(sample: Example<*>) = asWhere() alike sample + infix fun String.andDocumentStructureMatches(schema: MongoJsonSchema) = asWhere() andDocumentStructureMatches schema + + infix fun String.equal(where: Any?) = asWhere() equal where + infix fun String.eq(where: Any?) = asWhere() equal where + + infix fun String.equal(where: QueryBuilder.() -> Unit) = asWhere() equal QueryBuilder(where).queryObject + infix fun String.eq(where: QueryBuilder.() -> Unit) = asWhere() equal QueryBuilder(where).queryObject + + private infix fun Criteria.gt(where: Any) = gt(where).last(criteriaList::add) + + private infix fun Criteria.gte(where: Any) = gte(where).last(criteriaList::add) + + private infix fun Criteria.`in`(where: Array) = `in`(where).last(criteriaList::add) + private infix fun Criteria.`in`(where: Collection) = `in`(where).last(criteriaList::add) + + private infix fun Criteria.`is`(where: Any?) = `is`(where).last(criteriaList::add) + private infix fun Criteria.ne(where: Any?) = ne(where).last(criteriaList::add) + private infix fun Criteria.lt(where: Any) = lt(where).last(criteriaList::add) + private infix fun Criteria.lte(where: Any) = lte(where).last(criteriaList::add) + private infix fun Criteria.nin(where: Array) = nin(where).last(criteriaList::add) + private infix fun Criteria.nin(where: Collection) = nin(where).last(criteriaList::add) + + private infix fun Criteria.all(where: Any?) = all(where).last(criteriaList::add) + private infix fun Criteria.all(where: Collection) = all(where).last(criteriaList::add) + private infix fun Criteria.size(s: Int) = size(s).last(criteriaList::add) + private infix fun Criteria.exists(b: Boolean) = exists(b).last(criteriaList::add) + private infix fun Criteria.type(t: Int) = type(t).last(criteriaList::add) + + //private operator fun Criteria.not() = not().last(criteriaList::add) + private infix fun Criteria.regex(re: String) = regex(re).last(criteriaList::add) + private infix fun Criteria.regex(pattern: Pattern) = regex(pattern).last(criteriaList::add) + private infix fun Criteria.regex(pattern: Regex) = regex(pattern.toPattern()).last(criteriaList::add) + private infix fun Criteria.regex(regex: BsonRegularExpression) = regex(regex).last(criteriaList::add) + private infix fun Criteria.withinSphere(circle: Circle) = withinSphere(circle).last(criteriaList::add) + private infix fun Criteria.within(shape: Shape) = within(shape).last(criteriaList::add) + private infix fun Criteria.near(point: Point) = near(point).last(criteriaList::add) + private infix fun Criteria.nearSphere(point: Point) = nearSphere(point).last(criteriaList::add) + private infix fun Criteria.intersects(point: GeoJson<*>) = intersects(point).last(criteriaList::add) + private infix fun Criteria.maxDistance(maxDistance: Double) = maxDistance(maxDistance).last(criteriaList::add) + private infix fun Criteria.minDistance(maxDistance: Double) = minDistance(maxDistance).last(criteriaList::add) + private infix fun Criteria.elemMatch(c: QueryBuilder.() -> Unit) = + elemMatch(QueryBuilder criteria c).last(criteriaList::add) + + private infix fun Criteria.alike(sample: Example<*>) = alike(sample).last(criteriaList::add) + private infix fun Criteria.andDocumentStructureMatches(schema: MongoJsonSchema) = + andDocumentStructureMatches(schema).last(criteriaList::add) + + private infix fun Criteria.equal(where: Any?) = `is`(where).last(criteriaList::add) + + /** + * 根据 builder 所指定的规则排序 + */ + infix fun Query.sort(builder: SortBuilder.() -> Unit) = with(SortBuilder(builder)) + + /** + * 取以 field 为基准逆序排序的第一个元素 + */ + infix fun Query.last(field: KProperty<*>) = apply { + sort { field order Sort.Direction.DESC } + limit(1) + } + + /** + * 取以所有 field 为基准逆序排序的第一个元素 + */ + fun Query.last(vararg field: KProperty<*>) = last(field.iterator()) + + /** + * 取以所有 field 为基准逆序排序的第一个元素 + */ + @JvmName("lastFields") + fun Query.last(fields: Iterable>) = last(fields.iterator()) + + /** + * 取以所有 field 为基准逆序排序的第一个元素 + */ + @JvmName("lastFields") + fun Query.last(fields: Iterator>) = apply { + sort { fields.forEach { it order Sort.Direction.DESC } } + limit(1) + } + + /** + * 取以 field 为基准顺序排序的第一个元素 + */ + infix fun Query.first(field: KProperty<*>) = apply { + sort { field order Sort.Direction.ASC } + limit(1) + } + + /** + * 取以所有 field 为基准顺序排序的第一个元素 + */ + fun Query.first(vararg field: KProperty<*>) = first(field.iterator()) + + /** + * 取以所有 field 为基准顺序排序的第一个元素 + */ + @JvmName("firstFields") + fun Query.first(fields: Iterable>) = first(fields.iterator()) + + /** + * 取以所有 field 为基准顺序排序的第一个元素 + */ + @JvmName("firstFields") + fun Query.first(fields: Iterator>) = apply { + sort { fields.forEach { it order Sort.Direction.ASC } } + limit(1) + } + + infix fun Query.last(field: String) = apply { + sort { field order Sort.Direction.DESC } + limit(1) + } + + fun Query.last(vararg field: String) = last(field.iterator()) + fun Query.last(fields: Iterable) = last(fields.iterator()) + fun Query.last(fields: Iterator) = apply { + sort { fields.forEach { it order Sort.Direction.DESC } } + limit(1) + } + + infix fun Query.first(field: String) = apply { + sort { field order Sort.Direction.ASC } + limit(1) + } + + fun Query.first(vararg field: String) = first(field.iterator()) + fun Query.first(fields: Iterable) = first(fields.iterator()) + fun Query.first(fields: Iterator) = apply { + sort { fields.forEach { it order Sort.Direction.ASC } } + limit(1) + } + + val Query.fields get() = fields() + infix fun Field.include(key: KProperty<*>) = include(key.mongoName) + infix fun Field.exclude(key: KProperty<*>) = exclude(key.mongoName) + fun Field.slice(key: KProperty<*>, size: Int) = slice(key.mongoName, size) + fun Field.slice(key: KProperty<*>, offset: Int, size: Int) = slice(key.mongoName, offset, size) + fun Field.elemMatch(key: KProperty<*>, elemMatchCriteria: Criteria) = elemMatch(key.mongoName, elemMatchCriteria) + fun Field.position(field: KProperty<*>, value: Int) = position(mongoName(field), value) +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/SortBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/SortBuilder.kt new file mode 100644 index 0000000..920d1a2 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/SortBuilder.kt @@ -0,0 +1,41 @@ +package cn.tursom.database.mongodb.spring + +import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.core.aggregation.SortOperation +import kotlin.reflect.KProperty + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class SortBuilder : MongoName, BsonConverter { + companion object { + operator fun invoke(action: SortBuilder.() -> Unit): Sort { + val builder = SortBuilder() + builder.action() + return builder.sort!! + } + } + + private var sort: Sort? = null + val sortOperation get() = SortOperation(sort!!) + + operator fun invoke(action: SortBuilder.() -> Unit) = this.action() + + infix fun String.order(direction: Sort.Direction): Sort = Sort.by(Sort.Order(direction, this)).apply { + updateSort(this) { this.and(it) } + } + + infix fun KProperty<*>.order(direction: Sort.Direction) = mongoName order direction + + fun by(property: String) = Sort.by(property).apply { + updateSort(this) { this.and(it) } + } + + fun by(property: KProperty<*>) = by(property.mongoName) + + private fun updateSort(sort: Sort, action: (Sort) -> Sort) { + if (this.sort == null) { + this.sort = sort + } else { + this.sort = action(this.sort!!) + } + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilder.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilder.kt new file mode 100644 index 0000000..2fe6357 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilder.kt @@ -0,0 +1,109 @@ +package cn.tursom.database.mongodb.spring + +import org.bson.Document +import org.springframework.data.mongodb.core.query.Update +import kotlin.reflect.KProperty + +@Suppress("unused", "MemberVisibilityCanBePrivate", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "HasPlatformType") +class UpdateBuilder(val update: Update = Update()) : MongoName, BsonConverter { + companion object { + inline infix operator fun invoke(operator: UpdateBuilder.() -> Unit): Update = + UpdateBuilder(Update()).also(operator).update + + inline infix fun updateObject(operator: UpdateBuilder.() -> Unit): Document = + UpdateBuilder(Update()).also(operator).update.updateObject + + private val addMultiFieldOperationMethod = Update::class.java.getDeclaredMethod( + "addMultiFieldOperation", + String::class.java, + String::class.java, + Any::class.java + ) + + init { + addMultiFieldOperationMethod.isAccessible = true + } + + private fun Update.addMultiFieldOperation(operator: String, key: String, value: Any?) { + addMultiFieldOperationMethod.invoke(this, operator, key, value) + } + } + + infix fun String.set(value: Any?) = update.set(this, value?.bsonValue()) + infix fun String.setOnInsert(value: Any?) = update.setOnInsert(this, value?.bsonValue()) + + fun String.unset() = update.unset(this) + infix fun String.inc(inc: Number) = update.inc(this, inc) + fun String.inc() = update.inc(this, 1) + infix fun String.push(value: Any?) = update.push(this, value?.bsonValue()) + fun String.push() = update.push(this) + + @Suppress("DEPRECATION") + @Deprecated( + "as of MongoDB 2.4. Removed in MongoDB 3.6. Use {@link #push(String) \$push \$each} instead.", + ReplaceWith("push") + ) + infix fun String.pushAll(values: Array) = update.pushAll(this, values) + + fun String.addToSet() = update.addToSet(this) + infix fun String.addToSet(value: Any?) = update.addToSet(this, value?.bsonValue()) + infix fun String.pop(pos: Update.Position) = update.pop(this, pos) + infix fun String.pull(value: Any?) = update.pull(this, value?.bsonValue()) + infix fun String.pull(query: QueryBuilder.() -> Unit) = update.pull(this, QueryBuilder queryObject query) + infix fun String.pullAll(values: Array) = update.pullAll(this, values) + infix fun String.pullAll(values: Iterable) = update.addMultiFieldOperation("\$pullAll", this, values) + infix fun String.rename(newName: String) = update.rename(this, newName) + fun String.currentDate() = update.currentDate(this) + fun String.currentTimestamp() = update.currentTimestamp(this) + infix fun String.multiply(multiplier: Number) = update.multiply(this, multiplier) + infix fun String.max(value: Any) = update.max(this, value.bsonValue()) + infix fun String.min(value: Any) = update.min(this, value.bsonValue()) + fun String.bitwise() = update.bitwise(this) + fun String.modifies() = update.modifies(this) + + infix fun KProperty.set(value: T) = mongoName set value + infix fun KProperty.update(value: T) = mongoName set value + infix fun KProperty.setOnInsert(value: T) = mongoName setOnInsert value + fun KProperty<*>.unset() = mongoName.unset() + infix fun KProperty<*>.inc(inc: Number) = mongoName inc inc + fun KProperty<*>.inc() = mongoName inc (1) + infix fun KProperty<*>.push(value: Any) = mongoName push value + fun KProperty<*>.push() = mongoName.push() + + @Suppress("DEPRECATION") + @Deprecated( + "as of MongoDB 2.4. Removed in MongoDB 3.6. Use {@link #push(String) \$push \$each} instead.", + ReplaceWith("push") + ) + infix fun KProperty.pushAll(values: Array) = mongoName pushAll values + + fun KProperty<*>.addToSet() = mongoName.addToSet() + infix fun KProperty>.addToSet(value: T) = mongoName addToSet value + + @JvmName("addToSetArray") + infix fun KProperty>.addToSet(value: T) = mongoName addToSet value + infix fun KProperty<*>.pop(pos: Update.Position) = mongoName pop pos + infix fun KProperty>.pull(value: T) = mongoName pull value + + @JvmName("pullArray") + infix fun KProperty>.pull(value: T) = mongoName pull value + infix fun KProperty<*>.pull(query: QueryBuilder.() -> Unit) = + update.pull(mongoName, QueryBuilder queryObject query) + + infix fun KProperty>.pullAll(values: Array) = mongoName pullAll values + infix fun KProperty>.pullAll(values: Iterable) = mongoName pullAll values + + @JvmName("pullAllIterable") + infix fun KProperty>.pullAll(values: Array) = mongoName pullAll values + + @JvmName("pullAllIterable") + infix fun KProperty>.pullAll(values: Iterable) = mongoName pullAll values + infix fun KProperty<*>.rename(newName: String) = mongoName rename newName + fun KProperty<*>.currentDate() = mongoName.currentDate() + fun KProperty<*>.currentTimestamp() = mongoName.currentTimestamp() + infix fun KProperty<*>.multiply(multiplier: Number) = mongoName multiply multiplier + infix fun KProperty<*>.max(value: Any) = mongoName max value + infix fun KProperty<*>.min(value: Any) = mongoName min value + fun KProperty<*>.bitwise() = mongoName.bitwise() + fun KProperty<*>.modifies() = mongoName.modifies() +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateOption.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateOption.kt new file mode 100644 index 0000000..a5e9df8 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/UpdateOption.kt @@ -0,0 +1,48 @@ +package cn.tursom.database.mongodb.spring + +import cn.tursom.core.uncheckedCast +import com.mongodb.client.model.Collation +import com.mongodb.client.model.UpdateOptions +import org.bson.conversions.Bson + +@Suppress("MemberVisibilityCanBePrivate", "unused") +class UpdateOption { + companion object { + operator fun invoke(builder: UpdateOption.() -> Unit): UpdateOptions = UpdateOption().apply(builder).updateOptions + } + + val updateOptions = UpdateOptions() + var arrayFilters: MutableList? + @JvmName("arrayFiltersGetter") + get() = updateOptions.arrayFilters.uncheckedCast() + set(value) { + updateOptions.arrayFilters(value) + } + + fun getArrayFilters(): MutableList { + var list = arrayFilters + if (list == null) { + list = ArrayList() + arrayFilters = list + } + return list + } + + fun arrayFilter(builder: QueryBuilder.() -> Unit) = getArrayFilters().add(QueryBuilder queryObject builder) + + var upsert: Boolean + get() = updateOptions.isUpsert + set(value) { + updateOptions.upsert(value) + } + var bypassDocumentValidation: Boolean? + get() = updateOptions.bypassDocumentValidation + set(value) { + updateOptions.bypassDocumentValidation(value) + } + var collation: Collation? + get() = updateOptions.collation + set(value) { + updateOptions.collation(value) + } +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/Function1.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/Function1.kt new file mode 100644 index 0000000..a6fc319 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/Function1.kt @@ -0,0 +1,5 @@ +package cn.tursom.database.mongodb.spring.function + +fun interface Function1 : SerializedFunction { + operator fun T.invoke(): R +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/SerializedFunction.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/SerializedFunction.kt new file mode 100644 index 0000000..297cabd --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/function/SerializedFunction.kt @@ -0,0 +1,51 @@ +package cn.tursom.database.mongodb.spring.function + +import java.io.Serializable +import java.lang.invoke.SerializedLambda +import java.lang.reflect.Field +import java.lang.reflect.Method + +interface SerializedFunction : Serializable + +private val getterRegex = Regex("^get[A-Z].*") +private val booleanGetterRegex = Regex("^is[A-Z].*") + +val SerializedFunction.serializedLambda: SerializedLambda + get() { + val writeReplace = javaClass.getDeclaredMethod("writeReplace") + writeReplace.isAccessible = true + return writeReplace.invoke(this) as SerializedLambda + } + +val SerializedFunction.implClassName: String get() = serializedLambda.implClass.replace('/', '.') + +val SerializedFunction.implClass: Class<*> get() = Class.forName(implClassName) +val SerializedFunction.implMethodName: String get() = serializedLambda.implMethodName + +val SerializedFunction.implMethod: Method get() = implClass.getMethod(serializedLambda.implMethodName) +val SerializedFunction.fieldName: String? + get() { + val methodName = implMethodName + return getFieldNameFromGetterName(methodName) + } + +val SerializedFunction.field: Field? + get() { + val fieldName = fieldName ?: return null + val clazz = implClass + return clazz.getDeclaredField(fieldName) + } + +fun getFieldNameFromGetterName(getterName: String): String? = when { + getterRegex.matches(getterName) -> { + val chars = getterName.toCharArray() + chars[3] = Character.toLowerCase(chars[3]) + String(chars, 3, chars.size - 3) + } + booleanGetterRegex.matches(getterName) -> { + val chars = getterName.toCharArray() + chars[2] = Character.toLowerCase(chars[2]) + String(chars, 2, chars.size - 2) + } + else -> getterName +} \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/utils.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/utils.kt new file mode 100644 index 0000000..ffed849 --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/main/kotlin/cn/tursom/database/mongodb/spring/utils.kt @@ -0,0 +1,87 @@ +@file:Suppress("unused") + +package cn.tursom.database.mongodb.spring + +import com.mongodb.client.result.DeleteResult +import com.mongodb.client.result.UpdateResult +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Field +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Update +import kotlin.reflect.KProperty + +inline fun MongoTemplate.count(query: Query) = count(query, T::class.java) +inline fun MongoTemplate.count( + noinline operator: QueryBuilder.() -> Unit, +) = count(QueryBuilder(operator), T::class.java) + +inline fun MongoTemplate.find(query: Query): List = find(query, T::class.java) +inline fun MongoTemplate.find( + noinline operator: QueryBuilder.() -> Unit, +): List = find(QueryBuilder(operator), T::class.java) + +inline fun MongoTemplate.findOne(query: Query): T? = findOne(query, T::class.java) +inline fun MongoTemplate.findOne( + noinline operator: QueryBuilder.() -> Unit, +): T? = findOne(QueryBuilder(operator), T::class.java) + +//inline fun T.use(action: (T) -> R): T { +// action(this) +// return this +//} + +inline fun T.last(action: (T) -> R) { + action(this) +} + +inline fun MongoTemplate.upsert( + query: Query, + update: Update, +): UpdateResult = upsert(query, update, T::class.java) + +inline fun MongoTemplate.upsert( + collectionName: String, + query: Query, + update: Update, +): UpdateResult = upsert(query, update, T::class.java, collectionName) + +inline fun MongoTemplate.updateFirst( + query: Query, + update: Update, +): UpdateResult = updateFirst(query, update, T::class.java) + +inline fun MongoTemplate.updateFirst( + collectionName: String, + query: Query, + update: Update, +): UpdateResult = updateFirst(query, update, T::class.java, collectionName) + +inline fun MongoTemplate.updateMulti( + query: Query, + update: Update, +): UpdateResult = updateMulti(query, update, T::class.java) + +inline fun MongoTemplate.updateMulti( + collectionName: String, + query: Query, + update: Update, +): UpdateResult = updateMulti(query, update, T::class.java, collectionName) + +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +inline fun MongoTemplate.remove( + queryBuilder: QueryBuilder.() -> Unit, +): DeleteResult = remove(QueryBuilder(queryBuilder), T::class.java) + +infix fun Query.sort(builder: SortBuilder.() -> Unit) { + with(SortBuilder(builder)) +} + +infix fun Field.include(key: KProperty<*>) = include(MongoName.mongoName(key)) +infix fun Field.exclude(key: KProperty<*>) = exclude(MongoName.mongoName(key)) +fun Field.slice(key: KProperty<*>, size: Int) = slice(MongoName.mongoName(key), size) +fun Field.slice(key: KProperty<*>, offset: Int, size: Int) = slice(MongoName.mongoName(key), offset, size) +fun Field.elemMatch(key: KProperty<*>, elemMatchCriteria: Criteria) = + elemMatch(MongoName.mongoName(key), elemMatchCriteria) + +fun Field.position(field: KProperty<*>, value: Int) = position(MongoName.mongoName(field), value) \ No newline at end of file diff --git a/ts-database/ts-mongodb/ts-mongodb-spring/src/test/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilderTest.kt b/ts-database/ts-mongodb/ts-mongodb-spring/src/test/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilderTest.kt new file mode 100644 index 0000000..f29075e --- /dev/null +++ b/ts-database/ts-mongodb/ts-mongodb-spring/src/test/kotlin/cn/tursom/database/mongodb/spring/UpdateBuilderTest.kt @@ -0,0 +1,13 @@ +package cn.tursom.database.mongodb.spring + +class UpdateBuilderTest { + data class Test(val a: String, val b: Int) + + @org.junit.Test + fun testPush() { + println(UpdateBuilder { + Test::a push Test("a", 1) + Test::b set 1 + }) + } +} \ No newline at end of file