Skip to content

Commit 6fd9da6

Browse files
add batch save and update for r2dbc
1 parent 7d01671 commit 6fd9da6

File tree

8 files changed

+308
-60
lines changed

8 files changed

+308
-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: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,156 @@
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
1016
import org.springframework.stereotype.Repository
1117
import reactor.core.publisher.Flux
1218
import reactor.core.publisher.Mono
19+
import java.util.*
20+
import kotlin.reflect.KProperty1
21+
import kotlin.reflect.full.findAnnotation
22+
import kotlin.reflect.full.memberProperties
23+
import kotlin.reflect.jvm.javaField
1324

1425
@Repository
15-
interface PermissionRepository : R2dbcRepository<Permission, Long> {//, QuerydslPredicateExecutor<Permission> {
26+
interface PermissionRepository : R2dbcRepository<Permission, Long>, BatchPermissionRepository<Permission> {
1627
fun findBy(pageable: Pageable): Flux<Permission>
1728

1829
@Query("SELECT p.version FROM permissions as p WHERE p.id = :id")
1930
fun findVersionById(@Param("id") id: Long): Mono<Long>
2031
}
2132

33+
/**
34+
* Batch operations for @Table annotated entities
35+
*/
36+
interface BatchPermissionRepository<T>
37+
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 <T : BaseEntity> BatchPermissionRepository<T>.saveAllReturningIdsByUuid(
43+
entities: List<T>,
44+
databaseClient: DatabaseClient
45+
): Map<T, Long> {
46+
if (entities.isEmpty()) return emptyMap()
47+
48+
val sample = entities.first()
49+
val tableName = sample::class.findAnnotation<Table>()?.value
50+
?: error("Missing @Table annotation on ${sample::class.simpleName}")
51+
52+
val properties = sample::class.memberProperties
53+
.filter { it.javaField?.isAnnotationPresent(Column::class.java) == true }
54+
.map { prop -> prop to prop.javaField!!.getAnnotation(Column::class.java).value }
55+
56+
val activeProps = properties.filter { (prop, _) ->
57+
entities.any { (prop as KProperty1<T, *>).get(it) != null }
58+
}
59+
60+
val columnNames = activeProps.map { it.second }
61+
val valuePlaceholders = entities.mapIndexed { idx, _ ->
62+
"(" + activeProps.joinToString(", ") { (prop, _) -> ":${prop.name}$idx" } + ")"
63+
}.joinToString(", ")
64+
65+
val sql =
66+
"INSERT INTO $tableName (${columnNames.joinToString(", ")}) VALUES $valuePlaceholders RETURNING id, uuid"
67+
68+
var spec = databaseClient.sql(sql)
69+
70+
entities.forEachIndexed { idx, entity ->
71+
activeProps.forEach { (prop, _) ->
72+
val value = (prop as KProperty1<T, *>).get(entity)
73+
spec = spec.bind("${prop.name}$idx", value)
74+
}
75+
}
76+
77+
return specEntitiesToIds(spec, entities)
78+
}
79+
80+
/**
81+
* Updates all @Column annotated props except id and uuid, entities must have the same uuid
82+
* Returns map of updated entities to their db ids
83+
*/
84+
suspend fun <T : BaseEntity> BatchPermissionRepository<T>.updateAllReturningIdsByUuid(
85+
entities: List<T>,
86+
databaseClient: DatabaseClient
87+
): Map<T, Long> {
88+
if (entities.isEmpty()) return emptyMap()
89+
90+
val sample = entities.first()
91+
val tableName = sample::class.findAnnotation<Table>()?.value
92+
?: error("Missing @Table annotation on ${sample::class.simpleName}")
93+
94+
val properties = sample::class.memberProperties
95+
.filter { it.javaField?.isAnnotationPresent(Column::class.java) == true }
96+
.map { prop -> prop to prop.javaField!!.getAnnotation(Column::class.java).value }
97+
.filter { (_, colName) -> colName != "id" && colName != "uuid" }
98+
99+
val setClauses = properties.mapNotNull { (prop, colName) ->
100+
val cases = entities.mapIndexed { idx, entity ->
101+
val value = (prop as KProperty1<T, *>).get(entity)
102+
if (value != null) {
103+
"WHEN :uuid$idx THEN :${prop.name}$idx"
104+
} else null
105+
}.filterNotNull()
106+
107+
if (cases.isEmpty()) null
108+
else "$colName = CASE uuid ${cases.joinToString(" ")} ELSE $colName END"
109+
}
110+
111+
if (setClauses.isEmpty()) return emptyMap()
112+
113+
val sql = """
114+
UPDATE $tableName
115+
SET ${setClauses.joinToString(", ")}
116+
WHERE uuid IN (${entities.indices.joinToString(", ") { ":uuid$it" }})
117+
RETURNING id, uuid
118+
""".trimIndent()
119+
120+
var spec = databaseClient.sql(sql)
121+
122+
entities.forEachIndexed { idx, entity ->
123+
spec = spec.bind("uuid$idx", entity.uuid)
124+
properties.forEach { (prop, _) ->
125+
val value = (prop as KProperty1<T, *>).get(entity)
126+
if (value != null) {
127+
spec = spec.bind("${prop.name}$idx", value)
128+
}
129+
}
130+
}
131+
132+
return specEntitiesToIds(spec, entities)
133+
}
134+
135+
private suspend inline fun <T : BaseEntity> specEntitiesToIds(
136+
spec: DatabaseClient.GenericExecuteSpec,
137+
entities: List<T>
138+
): Map<T, Long> {
139+
val rows = spec.map { row, _ ->
140+
val uuid = row.get("uuid", UUID::class.java)!!
141+
val id = row.get("id", java.lang.Long::class.java)!!.toLong()
142+
uuid to id
143+
}
144+
.all()
145+
.collectList()
146+
.awaitSingle()
147+
148+
val uuidToId = rows.toMap()
149+
return entities.associateWith { uuidToId[it.uuid]!! }
150+
}
151+
152+
153+
22154
fun getPageRequest(page: Int, size: Int, sort: String, direction: String) =
23155
Sort.by(Sort.Order(if (direction == "ASC") Sort.Direction.ASC else Sort.Direction.DESC, sort))
24156
.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)