Skip to content

Commit 9dadf64

Browse files
add batch save and update for r2dbc
1 parent 7d01671 commit 9dadf64

File tree

8 files changed

+302
-60
lines changed

8 files changed

+302
-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: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,59 @@
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+
)
1311

1412
@Table(value = "permissions")
1513
data class Permission(
16-
val uuid: UUID,
14+
@Column(value = "uuid")
15+
override val uuid: UUID,
1716

18-
@Id
19-
var id: Long? = null,
17+
@Id
18+
@Column(value = "id")
19+
var id: Long? = null,
2020

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

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

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

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

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

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

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

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

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-
}
56+
override fun hashCode() = uuid.hashCode()
5757

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

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: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,158 @@
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 inline 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+
val sample = entities.first()
75+
val tableName = sample::class.findAnnotation<Table>()?.value
76+
?: error("Missing @Table annotation on ${sample::class.simpleName}")
77+
78+
val properties = sample::class.memberProperties
79+
.filter { it.javaField?.isAnnotationPresent(Column::class.java) == true }
80+
.map { prop -> prop to prop.javaField!!.getAnnotation(Column::class.java).value }
81+
82+
val activeProps = properties.filter { (prop, _) ->
83+
entities.any { (prop as KProperty1<T, *>).get(it) != null }
84+
}
85+
86+
val columnNames = activeProps.map { it.second }
87+
val valuePlaceholders = entities.mapIndexed { idx, _ ->
88+
"(" + activeProps.joinToString(", ") { (prop, _) -> ":${prop.name}$idx" } + ")"
89+
}.joinToString(", ")
90+
91+
val sql =
92+
"INSERT INTO $tableName (${columnNames.joinToString(", ")}) VALUES $valuePlaceholders RETURNING id, uuid"
93+
94+
var spec = databaseClient.sql(sql)
95+
96+
entities.forEachIndexed { idx, entity ->
97+
activeProps.forEach { (prop, _) ->
98+
val value = (prop as KProperty1<T, *>).get(entity)
99+
spec = spec.bind("${prop.name}$idx", value)
100+
}
101+
}
102+
103+
return specEntitiesToIds(spec, entities)
104+
}
105+
106+
override suspend fun updateAllReturningIds(entities: List<T>): Map<T, Long> {
107+
if (entities.isEmpty()) return emptyMap()
108+
109+
val sample = entities.first()
110+
val tableName = sample::class.findAnnotation<Table>()?.value
111+
?: error("Missing @Table annotation on ${sample::class.simpleName}")
112+
113+
val properties = sample::class.memberProperties
114+
.filter { it.javaField?.isAnnotationPresent(Column::class.java) == true }
115+
.map { prop -> prop to prop.javaField!!.getAnnotation(Column::class.java).value }
116+
.filter { (_, colName) -> colName != "id" && colName != "uuid" }
117+
118+
val setClauses = properties.mapNotNull { (prop, colName) ->
119+
val cases = entities.mapIndexed { idx, entity ->
120+
val value = (prop as KProperty1<T, *>).get(entity)
121+
if (value != null) {
122+
"WHEN :uuid$idx THEN :${prop.name}$idx"
123+
} else null
124+
}.filterNotNull()
125+
126+
if (cases.isEmpty()) null
127+
else "$colName = CASE uuid ${cases.joinToString(" ")} ELSE $colName END"
128+
}
129+
130+
if (setClauses.isEmpty()) return emptyMap()
131+
132+
val sql = """
133+
UPDATE $tableName
134+
SET ${setClauses.joinToString(", ")}
135+
WHERE uuid IN (${entities.indices.joinToString(", ") { ":uuid$it" }})
136+
RETURNING id, uuid
137+
""".trimIndent()
138+
139+
var spec = databaseClient.sql(sql)
140+
141+
entities.forEachIndexed { idx, entity ->
142+
spec = spec.bind("uuid$idx", entity.uuid)
143+
properties.forEach { (prop, _) ->
144+
val value = (prop as KProperty1<T, *>).get(entity)
145+
if (value != null) {
146+
spec = spec.bind("${prop.name}$idx", value)
147+
}
148+
}
149+
}
150+
151+
return specEntitiesToIds(spec, entities)
152+
}
153+
}
154+
155+
22156
fun getPageRequest(page: Int, size: Int, sort: String, direction: String) =
23157
Sort.by(Sort.Order(if (direction == "ASC") Sort.Direction.ASC else Sort.Direction.DESC, sort))
24158
.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)