Skip to content

Commit 5bb0a82

Browse files
add batch save and update for r2dbc
1 parent 7d01671 commit 5bb0a82

File tree

8 files changed

+339
-60
lines changed

8 files changed

+339
-60
lines changed

http/user-app.http

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,11 @@ GET {{host}}/permissions/{{permissionId}}
2020
Authorization: Bearer {{oauthToken}}
2121

2222
###
23-
PUT {{host}}/permissions/5
23+
PUT {{host}}/permissions/1
2424
Content-Type: application/json
2525
Authorization: Bearer {{oauthToken}}
2626

27-
{"name": "test-coroutine-changed", "description": "test description coroutine changed!", "version": 0}
28-
29-
###
30-
PUT {{host}}/permissions/{{permissionId}}/jpql
31-
Content-Type: application/json
32-
Authorization: Bearer {{oauthToken}}
33-
34-
{"name": "test-coroutine-changed", "description": "test description coroutine changed via jpql!", "version": 0}
27+
{"name": "test-coroutine-changed 22", "description": "test description coroutine changed!", "version": 1}
3528

3629
###
3730
DELETE {{host}}/permissions/{{permissionId}}
Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,60 @@
11
package com.softeno.template.app.permission
22

3-
import org.springframework.data.annotation.CreatedBy
4-
import org.springframework.data.annotation.CreatedDate
5-
import org.springframework.data.annotation.Id
6-
import org.springframework.data.annotation.LastModifiedBy
7-
import org.springframework.data.annotation.LastModifiedDate
8-
import org.springframework.data.annotation.Version
3+
import org.springframework.data.annotation.*
4+
import org.springframework.data.relational.core.mapping.Column
95
import org.springframework.data.relational.core.mapping.Table
10-
import java.util.UUID
11-
import kotlin.jvm.javaClass
6+
import java.util.*
127

8+
open class BaseEntity(
9+
open val uuid: UUID,
10+
open var id: Long? = null,
11+
)
1312

1413
@Table(value = "permissions")
1514
data class Permission(
16-
val uuid: UUID,
15+
@Column(value = "uuid")
16+
override val uuid: UUID,
1717

18-
@Id
19-
var id: Long? = null,
18+
@Id
19+
@Column(value = "id")
20+
override var id: Long? = null,
2021

21-
@CreatedDate
22-
val createdDate: Long? = null,
22+
@CreatedDate
23+
@Column(value = "created_date")
24+
val createdDate: Long? = null,
2325

24-
@LastModifiedDate
25-
val modifiedDate: Long? = null,
26+
@LastModifiedDate
27+
@Column(value = "modified_date")
28+
val modifiedDate: Long? = null,
2629

27-
@CreatedBy
28-
val createdBy: String? = null,
30+
@CreatedBy
31+
@Column(value = "created_by")
32+
val createdBy: String? = null,
2933

30-
@LastModifiedBy
31-
val modifiedBy: String? = null,
34+
@LastModifiedBy
35+
@Column(value = "modified_by")
36+
val modifiedBy: String? = null,
3237

33-
@Version
34-
val version: Long = 0,
38+
@Version
39+
@Column(value = "version")
40+
val version: Long = 0,
3541

36-
val name: String,
42+
@Column(value = "name")
43+
val name: String,
3744

38-
val description: String)
39-
{
40-
constructor(name: String, description: String) :
41-
this(
42-
uuid = UUID.randomUUID(),
43-
name = name,
44-
description = description)
45+
@Column(value = "description")
46+
val description: String
47+
) : BaseEntity(uuid) {
48+
constructor(name: String, description: String) :
49+
this(
50+
uuid = UUID.randomUUID(),
51+
name = name,
52+
description = description
53+
)
4554

46-
override fun equals(other: Any?): Boolean {
47-
return other is Permission && (uuid == other.uuid)
48-
}
55+
override fun equals(other: Any?) = other is Permission && (uuid == other.uuid)
4956

50-
override fun hashCode(): Int {
51-
return uuid.hashCode()
52-
}
53-
54-
override fun toString(): String {
55-
return "${javaClass.simpleName}(id = $id, uuid = $uuid, version = $version)"
56-
}
57+
override fun hashCode() = uuid.hashCode()
5758

59+
override fun toString() = "${javaClass.simpleName}(id = $id, uuid = $uuid, version = $version)"
5860
}

src/main/kotlin/com/softeno/template/app/permission/api/PermissionController.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.softeno.template.app.permission.mapper.PermissionDto
66
import com.softeno.template.app.permission.service.PermissionService
77
import io.micrometer.tracing.Tracer
88
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.reactor.awaitSingle
10+
import kotlinx.coroutines.reactor.awaitSingleOrNull
911
import org.apache.commons.logging.LogFactory
1012
import org.slf4j.MDC
1113
import org.springframework.http.ResponseEntity
@@ -60,8 +62,8 @@ class PermissionController(
6062
}
6163

6264
@PutMapping("/permissions/{id}")
63-
suspend fun updatePermission(@PathVariable id: Long, @RequestBody permissionDto: PermissionDto): ResponseEntity<PermissionDto> {
64-
val result = permissionService.updatePermission(id, permissionDto)
65+
suspend fun updatePermission(@PathVariable id: Long, @RequestBody permissionDto: PermissionDto, monoPrincipal: Mono<Principal>): ResponseEntity<PermissionDto> {
66+
val result = permissionService.updatePermission(id, permissionDto, principal = monoPrincipal.awaitSingle())
6567
return ResponseEntity.ok(result)
6668
}
6769

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,165 @@
11
package com.softeno.template.app.permission.db
22

3+
import com.softeno.template.app.permission.BaseEntity
34
import com.softeno.template.app.permission.Permission
5+
import kotlinx.coroutines.reactor.awaitSingle
46
import org.springframework.data.domain.PageRequest
57
import org.springframework.data.domain.Pageable
68
import org.springframework.data.domain.Sort
79
import org.springframework.data.r2dbc.repository.Query
810
import org.springframework.data.r2dbc.repository.R2dbcRepository
11+
import org.springframework.data.relational.core.mapping.Column
12+
import org.springframework.data.relational.core.mapping.Table
913
import 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
1017
import org.springframework.stereotype.Repository
1118
import reactor.core.publisher.Flux
1219
import 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+
22163
fun 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) }

src/main/kotlin/com/softeno/template/app/permission/service/PermissionService.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import org.springframework.data.relational.core.query.Query
2222
import org.springframework.data.relational.core.query.Update
2323
import org.springframework.stereotype.Service
2424
import org.springframework.transaction.annotation.Transactional
25+
import java.security.Principal
26+
import java.time.Instant
2527

2628
@Service
2729
class PermissionService(
@@ -50,17 +52,24 @@ class PermissionService(
5052
}
5153

5254
@Transactional
53-
suspend fun updatePermission(id: Long, permissionDto: PermissionDto): PermissionDto =
55+
suspend fun updatePermission(id: Long, permissionDto: PermissionDto, principal: Principal): PermissionDto =
5456
withContext(MDCContext()) {
55-
val currentVersion = permissionRepository.findVersionById(id).awaitSingle()
57+
val currentVersion = permissionRepository.findVersionById(id).awaitSingleOrNull()
58+
if (currentVersion == null) {
59+
throw RuntimeException("Not Found: $id, version: $currentVersion")
60+
}
61+
5662
if (currentVersion != permissionDto.version) {
5763
throw RuntimeException("Version mismatch")
5864
}
5965

6066
template.update(Permission::class.java).matching(Query.query(where("id").`is`(id)))
6167
.apply(Update.update("name", permissionDto.name)
6268
.set("description", permissionDto.description)
69+
.set("modified_by", principal.name)
70+
.set("modified_date", Instant.now().toEpochMilli())
6371
.set("version", permissionDto.version + 1)).awaitSingle()
72+
6473
return@withContext template.selectOne(Query.query(where("id").`is`(id)), Permission::class.java).awaitSingle().toDto()
6574
}
6675

src/main/resources/db/changesets/changelog-001.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,9 @@ databaseChangeLog:
2222
name: id
2323
type: BIGINT
2424
- column:
25-
constraints:
26-
nullable: false
2725
name: created_by
2826
type: VARCHAR(255)
2927
- column:
30-
constraints:
31-
nullable: false
3228
name: created_date
3329
type: BIGINT
3430
- column:

0 commit comments

Comments
 (0)