move repositories to separate db package

replace jacoco with kover
This commit is contained in:
Savosin Denis
2025-03-28 14:38:57 +07:00
parent 4f9ad14767
commit d7c051746d
37 changed files with 603 additions and 34 deletions

View File

@@ -0,0 +1,31 @@
package com.github.dannecron.demo.db.entity
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
import com.github.dannecron.demo.db.serialialization.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.UUID
@Table("city")
@Serializable
data class City(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun isDeleted(): Boolean = deletedAt != null
}

View File

@@ -0,0 +1,27 @@
package com.github.dannecron.demo.db.entity
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
import com.github.dannecron.demo.db.serialialization.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.UUID
@Table("customer")
@Serializable
data class Customer(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val cityId: Long?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
)

View File

@@ -0,0 +1,42 @@
package com.github.dannecron.demo.db.entity
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
import com.github.dannecron.demo.db.serialialization.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.math.pow
import kotlin.math.roundToInt
@Table(value = "product")
@Serializable
data class Product(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val description: String?,
val price: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2)
fun isDeleted(): Boolean = deletedAt != null
private fun Double.roundTo(numFractionDigits: Int): Double {
val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor
}
}

View File

@@ -0,0 +1,31 @@
package com.github.dannecron.demo.db.entity.order
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
import com.github.dannecron.demo.db.serialialization.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.UUID
@Table(value = "order")
@Serializable
data class Order(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val customerId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "delivered_at")
val deliveredAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?
) {
fun isDelivered(): Boolean = deliveredAt != null
}

View File

@@ -0,0 +1,41 @@
package com.github.dannecron.demo.db.entity.order
import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization
import com.github.dannecron.demo.db.serialialization.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.Transient
import org.springframework.data.domain.Persistable
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.UUID
@Table(value = "order_product")
@Serializable
data class OrderProduct(
@Id
@Serializable(with = UuidSerialization::class)
val guid: UUID,
@Column(value = "order_id")
val orderId: Long,
@Column(value = "product_id")
val productId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
): Persistable<UUID> {
@Transient
var isNewInstance: Boolean? = null
override fun getId(): UUID {
return guid
}
override fun isNew(): Boolean {
return isNewInstance ?: true
}
}

View File

@@ -0,0 +1,17 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.entity.City
import org.springframework.data.jdbc.repository.query.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import java.time.OffsetDateTime
import java.util.UUID
@Repository
interface CityRepository: CrudRepository<City, Long> {
fun findByGuid(guid: UUID): City?
@Query(value = "UPDATE city SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *")
fun softDelete(@Param("guid") guid: UUID, @Param("deletedAt") deletedAt: OffsetDateTime): City?
}

View File

@@ -0,0 +1,11 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.entity.Customer
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.UUID
@Repository
interface CustomerRepository: CrudRepository<Customer, Long> {
fun findByGuid(guid: UUID): Customer?
}

View File

@@ -0,0 +1,11 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.entity.order.OrderProduct
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.UUID
@Repository
interface OrderProductRepository: CrudRepository<OrderProduct, UUID> {
fun findByOrderId(orderId: Long): List<OrderProduct>
}

View File

@@ -0,0 +1,10 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.entity.order.Order
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
@Repository
interface OrderRepository: CrudRepository<Order, Long> {
fun findByCustomerId(customerId: Long): List<Order>
}

View File

@@ -0,0 +1,17 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.entity.Product
import org.springframework.data.jdbc.repository.query.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.PagingAndSortingRepository
import org.springframework.stereotype.Repository
import java.time.OffsetDateTime
import java.util.UUID
@Repository
interface ProductRepository: CrudRepository<Product, Long>, PagingAndSortingRepository<Product, Long> {
fun findByGuid(guid: UUID): Product?
@Query(value = "UPDATE Product SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *")
fun softDelete(guid: UUID, deletedAt: OffsetDateTime): Product?
}

View File

@@ -0,0 +1,19 @@
package com.github.dannecron.demo.db.serialialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
object OffsetDateTimeSerialization: KSerializer<OffsetDateTime> {
override val descriptor = PrimitiveSerialDescriptor("Time", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): OffsetDateTime = OffsetDateTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
encoder.encodeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

View File

@@ -0,0 +1,18 @@
package com.github.dannecron.demo.db.serialialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.UUID
object UuidSerialization: KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}

View File

@@ -0,0 +1,4 @@
insert into product (guid, name, description, price, created_at) values
(gen_random_uuid(), 'salt', 'simple salt', 1200, now()),
(gen_random_uuid(), 'pepper', 'black pepper', 2099, now()),
(gen_random_uuid(), 'sugar', 'sweet sugar', 3129, now());

View File

@@ -0,0 +1,4 @@
insert into city (guid, name, created_at) values
(gen_random_uuid(), 'Kemerovo', now()),
(gen_random_uuid(), 'Novosibirsk', now()),
(gen_random_uuid(), 'Krasnoyarsk', now());

View File

@@ -0,0 +1,9 @@
create table product (
id bigserial primary key,
guid uuid not null,
name varchar(255) not null,
description text,
price bigint not null,
created_at timestamptz not null,
updated_at timestamptz
)

View File

@@ -0,0 +1 @@
alter table product add deleted_at timestamptz default null;

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX product_guid_idx ON product (guid);

View File

@@ -0,0 +1,10 @@
create table city (
id bigserial primary key,
guid uuid not null,
name varchar(255) not null,
created_at timestamptz not null,
updated_at timestamptz,
deleted_at timestamptz
);
create unique index city_guid_idx ON city (guid);

View File

@@ -0,0 +1,14 @@
create table customer (
id bigserial primary key,
guid uuid not null,
name varchar(255) not null,
city_id bigint,
created_at timestamptz not null,
updated_at timestamptz,
CONSTRAINT customer_city_foreign
FOREIGN KEY(city_id)
REFERENCES city(id)
ON DELETE SET NULL
);
create unique index customer_guid_idx ON customer (guid);

View File

@@ -0,0 +1,14 @@
create table "order" (
id bigserial primary key,
guid uuid not null,
customer_id bigint not null,
delivered_at timestamptz,
created_at timestamptz not null,
updated_at timestamptz,
CONSTRAINT order_customer_foreign
FOREIGN KEY(customer_id)
REFERENCES customer(id)
ON DELETE CASCADE
);
create unique index order_guid_idx ON "order" (guid);

View File

@@ -0,0 +1,15 @@
create table order_product (
guid uuid primary key,
order_id bigint not null,
product_id bigint not null,
created_at timestamptz not null,
updated_at timestamptz,
CONSTRAINT order_product_order_foreign
FOREIGN KEY(order_id)
REFERENCES "order"(id)
ON DELETE CASCADE,
CONSTRAINT order_product_product_foreign
FOREIGN KEY(product_id)
REFERENCES product(id)
ON DELETE CASCADE
);

View File

@@ -0,0 +1,14 @@
package com.github.dannecron.demo.db
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories
import org.springframework.test.context.ActiveProfiles
import org.testcontainers.junit.jupiter.Testcontainers
@ActiveProfiles("db")
@DataJdbcTest
@Testcontainers(disabledWithoutDocker = false)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@EnableJdbcRepositories
class BaseDbTest

View File

@@ -0,0 +1,49 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.BaseDbTest
import com.github.dannecron.demo.db.entity.City
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.jdbc.Sql
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@ContextConfiguration(classes = [CityRepository::class])
class CityRepositoryTest : BaseDbTest() {
@Autowired
private lateinit var cityRepository: CityRepository
private val cityGuid = UUID.fromString("21a1a3a8-621a-40f7-b64f-7e118aa241b9")
private val city = City(
id = 1000,
guid = cityGuid,
name = "Tokyo",
createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"),
updatedAt = null,
deletedAt = null,
)
@Test
@Sql(scripts = ["/sql/insert_city.sql"])
fun findByGuid() {
val result = cityRepository.findByGuid(cityGuid)
assertEquals(city, result)
val emptyResult = cityRepository.findByGuid(UUID.randomUUID())
assertNull(emptyResult)
}
@Test
@Sql(scripts = ["/sql/insert_city.sql"])
fun softDelete() {
val deletedAt = OffsetDateTime.parse("2025-01-02T12:10:05+00:00")
val expectedCity = city.copy(deletedAt = deletedAt)
val result = cityRepository.softDelete(cityGuid, deletedAt)
assertEquals(expectedCity, result)
}
}

View File

@@ -0,0 +1,39 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.BaseDbTest
import com.github.dannecron.demo.db.entity.Customer
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.jdbc.Sql
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@ContextConfiguration(classes = [CustomerRepository::class])
class CustomerRepositoryTest : BaseDbTest() {
@Autowired
private lateinit var customerRepository: CustomerRepository
private val customerGuid = UUID.fromString("823c50de-4c81-49bd-a69a-2d52be42b728")
private val customer = Customer(
id = 1000,
guid = customerGuid,
name = "Customer",
cityId = 1000,
createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"),
updatedAt = null,
)
@Test
@Sql(scripts = ["/sql/insert_city.sql", "/sql/insert_customer.sql"])
fun findByGuid() {
val result = customerRepository.findByGuid(customerGuid)
assertEquals(customer, result)
val emptyResult = customerRepository.findByGuid(UUID.randomUUID())
assertNull(emptyResult)
}
}

View File

@@ -0,0 +1,43 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.BaseDbTest
import com.github.dannecron.demo.db.entity.order.OrderProduct
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.jdbc.Sql
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
@ContextConfiguration(classes = [OrderProductRepository::class])
class OrderProductRepositoryTest : BaseDbTest() {
@Autowired
private lateinit var orderProductRepository: OrderProductRepository
private val orderId = 1000L
private val orderProduct = OrderProduct(
guid = UUID.fromString("930f54e2-c60d-448e-83b1-0d259ff2c2d3"),
orderId = orderId,
productId = 1000,
createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"),
updatedAt = null,
)
@Test
@Sql(
scripts = [
"/sql/insert_city.sql",
"/sql/insert_customer.sql",
"/sql/insert_order.sql",
"/sql/insert_product.sql",
"/sql/insert_order_product.sql",
]
)
fun findByOrderId() {
val result = orderProductRepository.findByOrderId(orderId)
assertEquals(1, result.size)
assertEquals(orderProduct, result[0])
}
}

View File

@@ -0,0 +1,43 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.BaseDbTest
import com.github.dannecron.demo.db.entity.order.Order
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.jdbc.Sql
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
@ContextConfiguration(classes = [OrderRepository::class])
class OrderRepositoryTest : BaseDbTest() {
@Autowired
private lateinit var orderRepository: OrderRepository
private val orderGuid = UUID.fromString("2c960a08-7187-4e91-9ef3-275c91b1342c")
private val customerId = 1000L
private val order = Order(
id = 1000,
guid = orderGuid,
customerId = customerId,
deliveredAt = null,
createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"),
updatedAt = null,
)
@Test
@Sql(
scripts = [
"/sql/insert_city.sql",
"/sql/insert_customer.sql",
"/sql/insert_order.sql",
]
)
fun findByGuid() {
val result = orderRepository.findByCustomerId(customerId)
assertEquals(1, result.size)
assertEquals(order, result[0])
}
}

View File

@@ -0,0 +1,41 @@
package com.github.dannecron.demo.db.repository
import com.github.dannecron.demo.db.BaseDbTest
import com.github.dannecron.demo.db.entity.Product
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.jdbc.Sql
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
@ContextConfiguration(classes = [ProductRepository::class])
class ProductRepositoryTest : BaseDbTest() {
@Autowired
private lateinit var productRepository: ProductRepository
private val productGuid = UUID.fromString("1fb5c7e4-8ce2-43b8-8ca7-1089b04959b9")
private val product = Product(
id = 1000,
guid = productGuid,
name = "product",
description = "description",
price = 10000,
createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"),
updatedAt = null,
deletedAt = null,
)
@Test
@Sql(scripts = ["/sql/insert_product.sql"])
fun findByGuid() {
val result = productRepository.findByGuid(productGuid)
assertEquals(product, result)
val emptyResult = productRepository.findByGuid(UUID.randomUUID())
assertNull(emptyResult)
}
}

View File

@@ -0,0 +1,10 @@
---
spring:
datasource:
url: jdbc:tc:postgresql:14-alpine:///test
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
hikari:
maximum-pool-size: 2
flyway:
enabled: true
locations: classpath:migration/structure, classpath:migration/data

View File

@@ -0,0 +1,3 @@
insert into "city" (id, guid, name, created_at) values
(1000, '21a1a3a8-621a-40f7-b64f-7e118aa241b9', 'Tokyo', '2025-01-01 12:10:05 +00:00')
;

View File

@@ -0,0 +1,3 @@
insert into "customer" (id, guid, name, city_id, created_at) values
(1000, '823c50de-4c81-49bd-a69a-2d52be42b728', 'Customer', 1000, '2025-01-01 12:10:05 +00:00')
;

View File

@@ -0,0 +1,3 @@
insert into "order" (id, guid, customer_id, created_at) values
(1000, '2c960a08-7187-4e91-9ef3-275c91b1342c', 1000, '2025-01-01 12:10:05 +00:00')
;

View File

@@ -0,0 +1,3 @@
insert into "order_product" (guid, order_id, product_id, created_at) values
('930f54e2-c60d-448e-83b1-0d259ff2c2d3', 1000, 1000, '2025-01-01 12:10:05 +00:00')
;

View File

@@ -0,0 +1,3 @@
insert into "product" (id, guid, name, description, price, created_at) values
(1000, '1fb5c7e4-8ce2-43b8-8ca7-1089b04959b9', 'product', 'description', 10000, '2025-01-01 12:10:05 +00:00')
;