11package com.softeno.template.app.permission.db
22
3+ import com.softeno.template.app.permission.BaseEntity
34import com.softeno.template.app.permission.Permission
5+ import kotlinx.coroutines.reactor.awaitSingle
46import org.springframework.data.domain.PageRequest
57import org.springframework.data.domain.Pageable
68import org.springframework.data.domain.Sort
79import org.springframework.data.r2dbc.repository.Query
810import org.springframework.data.r2dbc.repository.R2dbcRepository
11+ import org.springframework.data.relational.core.mapping.Column
12+ import org.springframework.data.relational.core.mapping.Table
913import org.springframework.data.repository.query.Param
14+ import org.springframework.r2dbc.core.DatabaseClient
15+ import org.springframework.r2dbc.core.bind
16+ import org.springframework.stereotype.Component
1017import org.springframework.stereotype.Repository
1118import reactor.core.publisher.Flux
1219import reactor.core.publisher.Mono
20+ import java.util.*
21+ import kotlin.reflect.KProperty1
22+ import kotlin.reflect.full.findAnnotation
23+ import kotlin.reflect.full.memberProperties
24+ import kotlin.reflect.jvm.javaField
1325
1426@Repository
15- interface PermissionRepository : R2dbcRepository <Permission , Long > { // , QuerydslPredicateExecutor <Permission> {
27+ interface PermissionRepository : R2dbcRepository <Permission , Long >, BatchPermissionRepository <Permission > {
1628 fun findBy (pageable : Pageable ): Flux <Permission >
1729
1830 @Query(" SELECT p.version FROM permissions as p WHERE p.id = :id" )
1931 fun findVersionById (@Param(" id" ) id : Long ): Mono <Long >
2032}
2133
34+ /* *
35+ * Batch operations for @Table annotated entities
36+ */
37+ interface BatchPermissionRepository <T : BaseEntity > {
38+ /* *
39+ * Inserts all @Column annotated props except id and uuid
40+ * Returns map of inserted entities to their db ids
41+ */
42+ suspend fun insertAllReturningIds (entities : List <T >): Map <T , Long >
43+
44+ /* *
45+ * Updates all @Column annotated props except id and uuid, entities must have the same uuid
46+ * Returns map of updated entities to their db ids
47+ */
48+ suspend fun updateAllReturningIds (entities : List <T >): Map <T , Long >
49+ }
50+
51+ @Component
52+ class BatchPermissionRepositoryImpl <T : BaseEntity >(private val databaseClient : DatabaseClient ) : BatchPermissionRepository<T> {
53+
54+ private suspend fun <T : BaseEntity > specEntitiesToIds (
55+ spec : DatabaseClient .GenericExecuteSpec ,
56+ entities : List <T >
57+ ): Map <T , Long > {
58+ val rows = spec.map { row, _ ->
59+ val uuid = row.get(" uuid" , UUID ::class .java)!!
60+ val id = row.get(" id" , java.lang.Long ::class .java)!! .toLong()
61+ uuid to id
62+ }
63+ .all()
64+ .collectList()
65+ .awaitSingle()
66+
67+ val uuidToId = rows.toMap()
68+ return entities.associateWith { uuidToId[it.uuid]!! }
69+ }
70+
71+ override suspend fun insertAllReturningIds (entities : List <T >): Map <T , Long > {
72+ if (entities.isEmpty()) return emptyMap()
73+
74+ if (entities.any { it.id != null })
75+ throw OperationNotPermittedException (" Cannot insert entities with non null id" )
76+
77+ val sample = entities.first()
78+ val tableName = sample::class .findAnnotation<Table >()?.value
79+ ? : throw OperationNotPermittedException (" Missing @Table annotation on ${sample::class .simpleName} " )
80+
81+ val properties = sample::class .memberProperties
82+ .filter { it.javaField?.isAnnotationPresent(Column ::class .java) == true }
83+ .map { prop -> prop to prop.javaField!! .getAnnotation(Column ::class .java).value }
84+
85+ val activeProps = properties.filter { (prop, _) ->
86+ entities.any { (prop as KProperty1 <T , * >).get(it) != null }
87+ }
88+
89+ val columnNames = activeProps.map { it.second }
90+ val valuePlaceholders = entities.mapIndexed { idx, _ ->
91+ " (" + activeProps.joinToString(" , " ) { (prop, _) -> " :${prop.name}$idx " } + " )"
92+ }.joinToString(" , " )
93+
94+ val sql =
95+ " INSERT INTO $tableName (${columnNames.joinToString(" , " )} ) VALUES $valuePlaceholders RETURNING id, uuid"
96+
97+ var spec = databaseClient.sql(sql)
98+
99+ entities.forEachIndexed { idx, entity ->
100+ activeProps.forEach { (prop, _) ->
101+ val value = (prop as KProperty1 <T , * >).get(entity)
102+ spec = spec.bind(" ${prop.name}$idx " , value)
103+ }
104+ }
105+
106+ return specEntitiesToIds(spec, entities)
107+ }
108+
109+ override suspend fun updateAllReturningIds (entities : List <T >): Map <T , Long > {
110+ if (entities.isEmpty()) return emptyMap()
111+
112+ if (entities.any { it.id == null })
113+ throw OperationNotPermittedException (" Cannot update entities with null id" )
114+
115+ val sample = entities.first()
116+ val tableName = sample::class .findAnnotation<Table >()?.value
117+ ? : throw OperationNotPermittedException (" Missing @Table annotation on ${sample::class .simpleName} " )
118+
119+ val properties = sample::class .memberProperties
120+ .filter { it.javaField?.isAnnotationPresent(Column ::class .java) == true }
121+ .map { prop -> prop to prop.javaField!! .getAnnotation(Column ::class .java).value }
122+ .filter { (_, colName) -> colName != " id" && colName != " uuid" }
123+
124+ val setClauses = properties.mapNotNull { (prop, colName) ->
125+ val cases = entities.mapIndexed { idx, entity ->
126+ val value = (prop as KProperty1 <T , * >).get(entity)
127+ if (value != null ) {
128+ " WHEN :uuid$idx THEN :${prop.name}$idx "
129+ } else null
130+ }.filterNotNull()
131+
132+ if (cases.isEmpty()) null
133+ else " $colName = CASE uuid ${cases.joinToString(" " )} ELSE $colName END"
134+ }
135+
136+ if (setClauses.isEmpty()) return emptyMap()
137+
138+ val sql = """
139+ UPDATE $tableName
140+ SET ${setClauses.joinToString(" , " )}
141+ WHERE uuid IN (${entities.indices.joinToString(" , " ) { " :uuid$it " }} )
142+ RETURNING id, uuid
143+ """ .trimIndent()
144+
145+ var spec = databaseClient.sql(sql)
146+
147+ entities.forEachIndexed { idx, entity ->
148+ spec = spec.bind(" uuid$idx " , entity.uuid)
149+ properties.forEach { (prop, _) ->
150+ val value = (prop as KProperty1 <T , * >).get(entity)
151+ if (value != null ) {
152+ spec = spec.bind(" ${prop.name}$idx " , value)
153+ }
154+ }
155+ }
156+
157+ return specEntitiesToIds(spec, entities)
158+ }
159+ }
160+
161+ class OperationNotPermittedException (message : String ) : RuntimeException(message)
162+
22163fun getPageRequest (page : Int , size : Int , sort : String , direction : String ) =
23164 Sort .by(Sort .Order (if (direction == " ASC" ) Sort .Direction .ASC else Sort .Direction .DESC , sort))
24165 .let { PageRequest .of(page, size, it) }
0 commit comments