Spring Data JDBC
4 года в проде, полёт отличный
Что за прод?
2
Проект | Кол-во строк кода* | Кол-во таблиц |
№1 | 11 500 | 8 |
№2 | 7 000 | 16 |
№3 | 6 500 | 21 |
№4 | 39 000 | 66 |
№5** | 16 000 | 10 |
TA*** | 13 000 | 18 |
Итого | 87 000 | 139 |
* с учётом тестов
** проект в разработке
*** с открытым кодом- https://github.com/ergonomic-code/Trainer-Advisor
Все проекты не нагруженные:
Какую проблему решаем?
3
Как решаем
4
Функциональная архитектура =
Неизменяемая модель данных + разделение ввода-вывода и бизнес-логики
Неизменяемя модель данных:
Разделение io и логики:
ФА + SDJ = 💗
5
ФА требует неизменяемой модели данных (Java record, Kotlin data class) | SDJ рекомендует использование неизменяемых сущностей |
ФА требует декомпозиции модели на независимые элементы | SDJ требует декомпозиции модели на агрегаты |
ФА требует удаления циклов из модели | SDJ требует удаления циклов из модели |
ФА требует разделения IO и логики | SDJ не делает никакого IO под ковром |
ФА требует тщательного проектирования IO | SDJ требует тщательного проектирования IO |
Получилось ли решить?
6
Получилось ли решить?
7
Получилось ли решить?
8
Проблема №1:
агрегаты
9
Сущность
Операция
Агрегат
Проблема №1:
агрегаты
10
Две сущности объединяются в агрегат, если:
Проблема №1:
агрегаты
11
Алгоритм проектирования агрегатов:
Проблема №1:
агрегаты
12
Проблема №1:
агрегаты
13
Проблема №1:
агрегаты
14
Проблема №2: наследование
15
Проблема №3: загрузка нескольких агрегатов
16
data class Product(
val name: String,
val description: String,
val producer: AggregateReference<Producer, Long>,
@Id
val id: Long = 0,
@CreatedDate
val createdAt: Instant = Instant.now()
)
data class Producer(
val name: String,
val address: String,
@Id
val id: Long = 0,
@CreatedDate
val createdAt: Instant = Instant.now()
)
val product = productsRepo.findById(1L)
println(product.producer.name) 😭
val product = productsRepo.findById(1L)
val producer = producersRepo.findById(product.producer) 😱
println(producer.name)
17
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(Product::producer)))
?: error("Product not found: $productId")
println(product.producer.resolveOrThrow().name)😍
return product
}
18
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(Product::producer))
?: error("Product not found: $productId")
println(product.producer.resolveOrThrow().name)😍
return product
}
data class Product(
val name: String,
val description: String,
val producer: AggregateReference<Producer, Long>,� @Id
val id: Long = 0,
@CreatedDate
val createdAt: Instant = Instant.now()
)
19
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(Product::producer))
?: error("Product not found: $productId")
println(product.producer.resolveOrThrow().name)😍
return product
}
class SmartRepositoryImpl<T : Any, ID>(
private val jdbcAggregateTemplate: JdbcAggregateOperations,
private val entity: PersistentEntity<T, *>,
jdbcConverter: JdbcConverter,
) : SimpleJdbcRepository<T, ID>(jdbcAggregateTemplate, entity, jdbcConverter), SmartRepository<T, ID> {
override fun findById(id: ID, fetchSpec: FetchSpec<T>): T? {
val root = jdbcAggregateTemplate.findById(id!!, entity.type)
?: return null
return jdbcAggregateTemplate.hydrate(listOf(root), fetchSpec).single()
}
}
20
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(Product::producer))
?: error("Product not found: $productId")
return product
}
class SmartRepositoryImpl<T : Any, ID>(
private val jdbcAggregateTemplate: JdbcAggregateOperations,
private val entity: PersistentEntity<T, *>,
jdbcConverter: JdbcConverter,
) : SimpleJdbcRepository<T, ID>(jdbcAggregateTemplate, entity, jdbcConverter), SmartRepository<T, ID> {
override fun findById(id: ID, fetchSpec: FetchSpec<T>): T? {
val root = jdbcAggregateTemplate.findById(id!!, entity.type)
?: return null
return jdbcAggregateTemplate.hydrate(listOf(root), fetchSpec).single()
}
}
fun <T : Any> JdbcAggregateOperations.hydrate(
entities: Iterable<T>,
fetchSpec: FetchSpec<T>
): List<T> {
val refs: Map<KProperty1<*, AggregateReference<*, *>?>, Map<Any, Any>> =
fetchSpec.propertyFetchSpecs
.filter { detectRefType(it.property) != null }
.associate { it: PropertyFetchSpec<T, *> ->
val property = it.property as KProperty1<*, AggregateReference<*, *>?>
property to fetchPropertyRefs(entities, it)
}
if (refs.isEmpty()) {
return entities.toList()
}
return entities.map { hydrateEntity(it, refs)}
}
21
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(listOf(Product::producer)))
?: error("Product not found: $productId")
return product
}
class SmartRepositoryImpl<T : Any, ID>(
private val jdbcAggregateTemplate: JdbcAggregateOperations,
private val entity: PersistentEntity<T, *>,
jdbcConverter: JdbcConverter,
) : SimpleJdbcRepository<T, ID>(jdbcAggregateTemplate, entity, jdbcConverter), SmartRepository<T, ID> {
override fun findById(id: ID, fetchSpec: FetchSpec<T>): T? {
val root = jdbcAggregateTemplate.findById(id!!, entity.type)
?: return null
return jdbcAggregateTemplate.hydrate(listOf(root), fetchSpec).single()
}
}
fun <T : Any> JdbcAggregateOperations.hydrate(
entities: Iterable<T>,
fetchSpec: FetchSpec<T>
): List<T> {
val refs: Map<KProperty1<*, AggregateReference<*, *>?>, Map<Any, Any>> =
fetchSpec.propertyFetchSpecs
.filter { detectRefType(it.property) != null }
.associate { it: PropertyFetchSpec<T, *> ->
val property = it.property as KProperty1<*, AggregateReference<*, *>?>
property to fetchPropertyRefs(entities, it)
}
if (refs.isEmpty()) {
return entities.toList()
}
return entities.map { hydrateEntity(it, refs)}
}
data class PropertyFetchSpec<T : Any?, V>(
val property: KProperty1<T, V?>,
val fetchSpec: FetchSpec<*> = FetchSpec(
emptyList<PropertyFetchSpec<Any, Any>>()
)
)
data class FetchSpec<T : Any?>(
val propertyFetchSpecs: List<PropertyFetchSpec<T, *>>
) {
constructor(propertyFetchSpec: Iterable<KProperty1<T, *>>) : this(propertyFetchSpec.map {
PropertyFetchSpec(
it
)
})
}
fun <T : Any> JdbcAggregateOperations.hydrate(
entities: Iterable<T>,
fetchSpec: FetchSpec<T>
): List<T> {
val refs: Map<KProperty1<*, AggregateReference<*, *>?>, Map<Any, Any>> =
fetchSpec.propertyFetchSpecs
.filter {
detectRefType(
it.property
) != null
}
.associate { it: PropertyFetchSpec<T, *> ->
val property = it.property as KProperty1<*, AggregateReference<*, *>?>
property to fetchPropertyRefs(entities, it)
}
if (refs.isEmpty()) {
return entities.toList()
}
return entities.map {
hydrateEntity(it, refs)
}
}
private fun <T : Any> JdbcAggregateOperations.fetchPropertyRefs(
entities: Iterable<T>,
propertyFetchSpec: PropertyFetchSpec<T, *>
): Map<Any, Any> {
val property = propertyFetchSpec.property
val ids = fetchIds(entities, property)
val targetType = (property.returnType.arguments[0].type!!.classifier!! as KClass<*>).java
val refs = hydrate(this.findAllById(ids, targetType), propertyFetchSpec.fetchSpec as FetchSpec<Any>)
.associateBy { (it as Identifiable<*>).id }
return refs
}
private fun <T : Any> fetchIds(
entities: Iterable<T>,
property: KProperty1<T, Any?>
): Set<Any?> = when (detectRefType(property)) {
RefType.SCALAR ->
entities
.map { (property.getter.invoke(it) as AggregateReference<*, *>?)?.id }
.toSet()
else -> error("Unsupported property type: $property")
}
private fun <T : Any> hydrateEntity(entity: T, refs: Map<KProperty1<*, AggregateReference<*, *>?>, Map<Any, Any>>): T {
val constructorParams = entity::class.primaryConstructor!!.parameters
val paramValues = constructorParams.associateWith { param ->
val prop =
entity::class.memberProperties.find { prop -> param.name == prop.name }!! as KProperty1<T, AggregateReference<*, *>?>
val currentValue: Any? = prop.invoke(entity)
val newValue = if (prop in refs) {
val id = (currentValue as AggregateReference<*, *>?)?.id
val res: Any? = if (id != null) {
val ref = refs[prop]!![id] as Identifiable<Any>?
checkNotNull(ref) { "Aggregate ${prop.returnType.arguments[0]} not found by id=$id" }
val res: AggregateReferenceTarget<*, *> = AggregateReferenceTarget(ref)
res
} else {
null
}
res
} else {
currentValue
}
newValue
}
return entity::class.primaryConstructor!!.callBy(paramValues)
}
private fun detectRefType(property: KProperty1<*, *>): RefType? = when {
property.returnType.jvmErasure.isSubclassOf(AggregateReference::class) ->
RefType.SCALAR
else ->
null
}
enum class RefType {
SCALAR,
}
22
fun getProduct(productId: Long): Product {
val product = productsRepo.findById(productId, fetchSpec = FetchSpec(Product::producer)))
?: error("Product not found: $productId")
println(product.producer.resolveOrThrow().name)😍
return product
}
fun <R : AggregateReference<T, ID>?, ID : Any, T : Identifiable<ID>> R.resolveOrThrow(): T =
(this as? AggregateReferenceTarget<T, ID>)?.entity
?: error("$this is not instance of AggregateReferenceTarget")
Проблема №3: загрузка нескольких агрегатов
23
Проблема №4: динамические запросы
24
Проблема №4: динамические запросы
25
26
fun findProducts(
name: String?,
description: String?,
pageRequest: Pageable
): Page<Product> {
val byNameOrDescr = (Product::name isLikeIfNotNull name) OR
(Product::description isLikeIfNotNull description)
return productsRepo.findPage(Query.query(byNameOrDescr), pageRequest)
}
27
fun findProducts(
name: String?,
description: String?,
pageRequest: Pageable
): Page<Product> {
val byNameOrDescr = (Product::name isLikeIfNotNull name) OR
(Product::description isLikeIfNotNull description)
return productsRepo.findPage(Query.query(byNameOrDescr), pageRequest)
}
data class Product(
val name: String,
val description: String,
val producer: AggregateReference<Producer, Long>,
@Id
val id: Long = 0,
@CreatedDate
val createdAt: Instant = Instant.now()
)
28
infix fun <T, V> KProperty1<T, V>.isLikeIfNotNull(value: String?): Criteria {
return if (value != null) {
this.columnName().isLike(value)
} else {
Criteria.empty()
}
}
fun findProducts(
name: String?,
description: String?,
pageRequest: Pageable
): Page<Product> {
val byNameOrDescr = (Product::name isLikeIfNotNull name) OR
(Product::description isLikeIfNotNull description)
return productsRepo.findPage(Query.query(byNameOrDescr), pageRequest)
}
29
infix fun <T, V> KProperty1<T, V>.isLikeIfNotNull(value: String?): Criteria {
return if (value != null) {
this.columnName().isLike(value)
} else {
Criteria.empty()
}
}
fun findProducts(
name: String?,
description: String?,
pageRequest: Pageable
): Page<Product> {
val byNameOrDescr = (Product::name isLikeIfNotNull name) OR
(Product::description isLikeIfNotNull description)
return productsRepo.findPage(Query.query(byNameOrDescr), pageRequest)
}
infix fun Criteria.OR(criteria: Criteria): Criteria =
when {
this != Criteria.empty() && criteria != Criteria.empty() ->
this.or(criteria)
this == Criteria.empty() ->
criteria
else ->
this
}
30
infix fun <T, V> KProperty1<T, V>.isLikeIfNotNull(value: String?): Criteria {
return if (value != null) {
this.columnName().isLike(value)
} else {
Criteria.empty()
}
}
fun findProducts(
name: String?,
description: String?,
pageRequest: Pageable
): Page<Product> {
val byNameOrDescr = (Product::name isLikeIfNotNull name) OR
(Product::description isLikeIfNotNull description)
return productsRepo.findPage(Query.query(byNameOrDescr), pageRequest)
}
infix fun Criteria.OR(criteria: Criteria): Criteria =
when {
this != Criteria.empty() && criteria != Criteria.empty() ->
this.or(criteria)
this == Criteria.empty() ->
criteria
else ->
this
}
class SmartRepositoryImpl<T : Any, ID>(
private val jdbcAggregateTemplate: JdbcAggregateOperations,
private val operations: NamedParameterJdbcOperations,
private val entity: RelationalPersistentEntity<T>,
jdbcConverter: JdbcConverter,
) : SimpleJdbcRepository<T, ID>(jdbcAggregateTemplate, entity, jdbcConverter), SmartRepository<T, ID> {
protected val rowMapper = EntityRowMapper(entity, jdbcConverter)
override fun findPage(query: Query, pageRequest: Pageable): Page<T> {
return jdbcAggregateTemplate.findAll(query, entity.type, pageRequest)
}
31
fun findProducts(
name: String?,
description: String?,
producerName: String?,
pageRequest: Pageable
): Page<Product> {
val product = Table.create("product")
val producer = Table.create("producer")
val query = Select.builder()
.select(product.all())
.from(product)
.join(producer)
.on(create(product["producer"], "=", producer["id"]))
.where(TrueCondition.INSTANCE)
.andWhereIfPresent(name) {
Like.create(product["name"], literalOf("%$it%")) }
.andWhereIfPresent(description) {� Like.create(product["description"], literalOf("%$it%")) }
.andWhereIfPresent(producerName) {� Like.create(producer["name"], literalOf("%$it%")) }
.build()
.let { SqlRenderer.create().render(it) }
val params = mapOf(
"name" to name,
"description" to description,
"producerName" to producerName,
)
return productsRepo.findPage(query, params, pageRequest)
}
32
fun findProducts(
name: String?,
description: String?,
producerName: String?,
pageRequest: Pageable
): Page<Product> {
val product = Table.create("product")
val producer = Table.create("producer")
val query = Select.builder()
.select(product.all())
.from(product)
.join(producer)
.on(create(product["producer"], "=", producer["id"]))
.where(TrueCondition.INSTANCE)
.andWhereIfPresent(name) {
Like.create(product["name"], literalOf("%$it%")) }
.andWhereIfPresent(description) {� Like.create(product["description"], literalOf("%$it%")) }
.andWhereIfPresent(producerName) {� Like.create(producer["name"], literalOf("%$it%")) }
.build()
.let { SqlRenderer.create().render(it) }
val params = mapOf(
"name" to name,
"description" to description,
"producerName" to producerName,
)
return productsRepo.findPage(query, params, pageRequest)
}
fun SelectBuilder.SelectWhereAndOr.andWhereIfPresent(
param: String?,
condition: (String) -> Condition
): SelectBuilder.SelectWhereAndOr =
if (param != null) {
this.or(condition(param))
} else {
this
}
fun Table.all() =
AsteriskFromTable.create(this)
operator fun Table.get(columnName: String): Expression =
Column.create(columnName, this)
33
fun findProducts(
name: String?,
description: String?,
producerName: String?,
pageRequest: Pageable
): Page<Product> {
val product = Table.create("product")
val producer = Table.create("producer")
val query = Select.builder()
.select(product.all())
.from(product)
.join(producer)
.on(create(product["producer"], "=", producer["id"]))
.where(TrueCondition.INSTANCE)
.andWhereIfPresent(name) {
Like.create(product["name"], literalOf("%$it%")) }
.andWhereIfPresent(description) {� Like.create(product["description"], literalOf("%$it%")) }
.andWhereIfPresent(producerName) {� Like.create(producer["name"], literalOf("%$it%")) }
.build()
.let { SqlRenderer.create().render(it) }
val params = mapOf(
"name" to name,
"description" to description,
"producerName" to producerName,
)
return productsRepo.findPage(query, params, pageRequest)
}
class SmartRepositoryImpl<T : Any, ID>(
/* ** */
) : SimpleJdbcRepository<T, ID>(jdbcAggregateTemplate, entity, jdbcConverter), SmartRepository<T, ID> {
protected val rowMapper = EntityRowMapper(entity, jdbcConverter)
override fun findPage(
query: String,
paramMap: Map<String, Any?>,
pageRequest: Pageable,
): Page<T> {
// RowMapper из SDJ
return operations.queryForPage(query, paramMap, pageRequest, rowMapper)
}
}
34
fun findProducts(
name: String?,
description: String?,
producerName: String?,
pageRequest: Pageable
): Page<Product> {
val product = Table.create("product")
val producer = Table.create("producer")
val query = Select.builder()
.select(product.all())
.from(product)
.join(producer)
.on(create(product["producer"], "=", producer["id"]))
.where(TrueCondition.INSTANCE)
.andWhereIfPresent(name) {
Like.create(product["name"], literalOf("%$it%")) }
.andWhereIfPresent(description) {� Like.create(product["description"], literalOf("%$it%")) }
.andWhereIfPresent(producerName) {� Like.create(producer["name"], literalOf("%$it%")) }
.build()
.let { SqlRenderer.create().render(it) }
val params = mapOf(
"name" to name,
"description" to description,
"producerName" to producerName,
)
return productsRepo.findPage(query, params, pageRequest)
}
fun <T : Any> NamedParameterJdbcOperations.queryForPage(
baseSql: String,
filterParams: Map<String, Any?>,
pageRequest: Pageable,
rowMapper: RowMapper<T>
): Page<T> {
val count = this.queryForObject(
"SELECT count(*) FROM ($baseSql) AS data", filterParams, Long::class.java)
if (count == 0L) {
return PageImpl(emptyList(), pageRequest, count)
}
val dataQuery = buildString {
appendLine("SELECT * FROM($baseSql) data")
if (pageRequest.sort.isSorted) {
append("ORDER BY ")
appendLine(pageRequest.sort� .map { it.property + " " + it.direction.name }
.joinToString(", "))
}
if (pageRequest.isPaged) {
appendLine("""
LIMIT :pageSize
OFFSET :offset
"""
)
}
}
val pagingParams = if (pageRequest.isPaged) {
mapOf("pageSize" to pageRequest.pageSize, "offset" to pageRequest.offset)
} else {
emptyMap()
}
val data = this.query(
dataQuery,
filterParams + pagingParams,
rowMapper
)
return PageImpl(data, pageRequest, count)
}
Проблема №4: динамические запросы
35
Заключение
36