👀
Kotlin / Spring Boot / MongoDB / DDDでネストされているプロパティへデータを追加する
はじめに
お疲れ様です。
@いけふくろうです。
WebSocketを使ったリアルタイム性が求められる機能を作ることになったので、私自身としては初めてMongoDBを選定しました。
開発している機能でタイトルの実装が必要となったのですが、Spring Data MongoDBで用意されているデータアクセスのメソッドではなく、カスタムで実装する必要があったため、そのHowについてまとめてみました。
環境
- Kotlin 1.6.21
- Java 17
- Spring Boot 2.7.3
- MongoDB
実装したいこと
以下のようなデータ構造であるorder
コレクションに対して、
-
orderId
、userId
及びorderDetailId
に合致するproducts
プロパティの配列内にデータを追加したい
[
{
"orderId": "ede66c43-9b9d-4222-93ed-5f11c96e08e2",
"orderDate": "2022-08-30 10:00:00",
"userId": 1,
"userName": "poc taro",
"orderDetails": [
{
"orderDetailId": "d55847c2-5daa-6e28-fa4a-c3a434029aef",
"products": [
{
"productId": 1,
"productName": "みかん",
"price": 200,
"quantity": 2
},
{
"productId": 3,
"productName": "ぶどう",
"price": 400,
"quantity": 1
}
]
},
{
"orderDetailId": "989a52b9-d2c0-415a-af6f-e412c0e31f33",
"products": [
{
"productId": 101,
"productName": "ブロッコリー",
"price": 350,
"quantity": 2
}
]
}
]
},
{
"orderId": "9c412fcb-9155-4611-b03e-8b1a1a54b54b",
"orderDate": "2022-08-30 10:00:00",
"userId": 2,
"userName": "poc jiro",
"orderDetails": [
{
"orderDetailId": "5901cdf6-ad68-4009-80f5-04995e2dd16d",
"products": [
{
"productId": 2,
"productName": "りんご",
"price": 150,
"quantity": 1
}
]
}
]
}
]
環境構築
細かい部分の内容は省略しますが、Dockerで構築しました。
docker-compose.yml
version: '3'
services:
mongo:
image: poc-mongodb
container_name: poc-mongodb
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: password123
MONGO_INITDB_DATABASE: poc
TZ: Asia/Tokyo
ports:
- 27017:27017
volumes:
- ./docker/db/mongo/db:/data/db
build:
context: .
dockerfile: ./POC_Dockerfile_MongoDB
mongo-express:
image: mongo-express
container_name: poc-mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_SERVER: mongo
ME_CONFIG_MONGODB_PORT: 27017
ME_CONFIG_MONGODB_ENABLE_ADMIN: 'true'
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: password123
depends_on:
- mongo
POC_Dockerfile_MongoDB
FROM mongo:4
ARG LOCAL_APP_DIR="."
COPY ${LOCAL_APP_DIR}/docker/db/mongo/init /docker-entrypoint-initdb.d/
Dockerの構築用のタスク定義
Justfile
を作成しました。
今までは、Makefile
を使ってましたが、Rustで作られているモダンなMakefile
のようなものらしいので、cargo
でjust
コマンドをインストールしてセットアップしてみました!
Justfile
load:
docker-compose down --rmi all --volumes --remove-orphans
rm -rf ./docker/db/mongo/db/
docker-compose up -d --build
Docker構築
% just load
% docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
poc-mongo-express "tini -- /docker-ent…" mongo-express running 0.0.0.0:8081->8081/tcp
poc-mongodb "docker-entrypoint.s…" mongo running 0.0.0.0:27017->27017/tcp
%
コレクションの作成確認
http://0.0.0.0:8081/db/poc/order
Infrastructure層
本記事における主となる部分の実装です。
Entity定義
src/main/kotlin/com/ss/poc/infrastucture/order/ProductEntity.kt
data class ProductEntity(
val productId: Int,
val productName: String,
val price: Int,
val quantity: Int
) {
constructor(productDto: ProductDto) : this(
productDto.productId,
productDto.productName,
productDto.price,
productDto.quantity
)
}
- サービス層のクラスからはDTOで渡されるので、コンストラクタを用意しています
src/main/kotlin/com/ss/poc/infrastucture/order/OrderDetailEntity.kt
data class OrderDetailEntity(
val orderDetailId: String,
val products: List<ProductEntity>
)
src/main/kotlin/com/ss/poc/infrastucture/order/OrderEntity.kt
@Document("order")
data class OrderEntity(
val orderId: String,
val orderDate: String,
val userId: Int,
val userName: String,
val orderDetails: List<OrderDetailEntity>
)
- MongoDBのコレクションである
order
を紐づけています
Repository
src/main/kotlin/com/ss/poc/domain/order/IOrderCustomRepository.kt
interface IOrderCustomRepository {
fun fetchCountOrderByOrderIdAndUserId(orderId: String, userId: Int, orderDetailId: String): Long
fun updateOrderProductByOrderIdAndUserId(
orderId: String,
userId: Int,
orderDetailId: String,
productDto: ProductDto
): Boolean
}
src/main/kotlin/com/ss/poc/infrastucture/order/impl/OrderCustomRepository.kt
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
import org.springframework.data.mongodb.core.query.Update
@Repository
class OrderCustomRepository(private val mongoTemplate: MongoTemplate) : IOrderCustomRepository {
override fun fetchCountOrderByOrderIdAndUserId(orderId: String, userId: Int, orderDetailId: String): Long {
val query = buildQuery(orderId, userId, orderDetailId)
return mongoTemplate.count(query, OrderEntity::class.java)
}
override fun updateOrderProductByOrderIdAndUserId(
orderId: String,
userId: Int,
orderDetailId: String,
productDto: ProductDto
): Boolean {
val query = buildQuery(orderId, userId, orderDetailId)
val update = Update().push("orderDetails.$[element].products", ProductEntity(productDto))
.filterArray(Criteria.where("element.orderDetailId").`is`(orderDetailId))
val updateResult = mongoTemplate.updateFirst(query, update, OrderEntity::class.java)
return updateResult.modifiedCount > 0
}
private fun buildQuery(orderId: String, userId: Int, orderDetailId: String): Query {
val query = Query.query(Criteria.where("orderDetails.orderDetailId").`is`(orderDetailId))
query.addCriteria(Criteria.where("orderId").`is`(orderId).and("userId").`is`(userId))
return query
}
}
-
updateOrderProductByOrderIdAndUserId
が対象のメソッドです
val update = Update().push("orderDetails.$[element].products", ProductEntity(productDto))
.filterArray(Criteria.where("element.orderDetailId").`is`(orderDetailId))
特に、↑の部分がポイントでした。
<こちら>の公式などを頼りにしました。
配列の動的指定部分は、$[element]
とするようです。
アプリケーション・サービス
src/main/kotlin/com/ss/poc/application/order/ProductDto.kt
data class ProductDto(
val productId: Int,
val productName: String,
val price: Int,
val quantity: Int
) {
constructor(product: Product) : this(
product.productId,
product.productName,
product.price,
product.quantity
)
}
- ドメインクラスは省略しますが、ドメインからDTOへの詰め替え用クラスです
src/main/kotlin/com/ss/poc/application/order/OrderProductUpdateCommand.kt
data class OrderProductUpdateCommand(
val orderId: String,
val userId: Int,
val orderDetailId: String,
val productId: Int,
val productName: String,
val price: Int,
val quantity: Int
)
- パラメータをまとめたコマンドオブジェクトです
src/main/kotlin/com/ss/poc/application/order/impl/OrderService.kt
@Service
class OrderService(private val orderCustomRepository: IOrderCustomRepository) : IOrderService {
override fun updateOrderProduct(command: OrderProductUpdateCommand) {
// Productドメイン生成
val product = Product(command.productId, command.productName, command.price, command.quantity)
// 注文商品を追加する(追加されたかどうかを検証する必要あり)
val updateResult = orderCustomRepository.updateOrderProductByOrderIdAndUserId(
command.orderId,
command.userId,
command.orderDetailId,
ProductDto(product)
)
if (!updateResult) {
throw RuntimeException("追加に失敗しました >>> orderId: ${command.orderId} / userId: ${command.userId} / orderDetailId: ${command.orderDetailId}")
}
}
}
-
updateOrderProductByOrderIdAndUserId
を呼び出しています
プレゼンテーション
src/main/kotlin/com/ss/poc/presentation/order/UpdateOrderProductRequest.kt
data class UpdateOrderProductRequest(
@field: NotNull val orderId: String,
@field: Positive val userId: Int,
@field: NotNull val orderDetailId: String,
@field: Positive val productId: Int,
@field: NotNull val productName: String,
@field: PositiveOrZero val price: Int,
@field: NotNull val quantity: Int
)
- リクエストクラスです
src/main/kotlin/com/ss/poc/presentation/order/OrderController.kt
@RestController
@RequestMapping(value = ["api/v1/order"], produces = ["application/json"])
class OrderController(private val orderService: IOrderService) {
@PutMapping("/product")
fun updateOrderProduct(@RequestBody @Validated updateOrderProductRequest: UpdateOrderProductRequest) {
orderService.updateOrderProduct(
OrderProductUpdateCommand(
updateOrderProductRequest.orderId,
updateOrderProductRequest.userId,
updateOrderProductRequest.orderDetailId,
updateOrderProductRequest.productId,
updateOrderProductRequest.productName,
updateOrderProductRequest.price,
updateOrderProductRequest.quantity
)
)
}
}
- サービス層の
updateOrderProduct
を呼び出しています
API実行
order
{
_id: ObjectId('630f66427b550292745347a6'),
orderId: 'ede66c43-9b9d-4222-93ed-5f11c96e08e2',
orderDate: '2022-08-30 10:00:00',
userId: 1,
userName: 'poc taro',
orderDetails: [
{
orderDetailId: 'd55847c2-5daa-6e28-fa4a-c3a434029aef',
products: [
{
productId: 1,
productName: 'みかん',
price: 200,
quantity: 2
},
{
productId: 3,
productName: 'ぶどう',
price: 400,
quantity: 1
},
{
productId: 4,
productName: 'なし',
price: 500,
quantity: 2
}
]
},
{
orderDetailId: '989a52b9-d2c0-415a-af6f-e412c0e31f33',
products: [
{
productId: 101,
productName: 'ブロッコリー',
price: 350,
quantity: 2
}
]
}
]
}
以下のproduct
オブジェクトが追加された!!
おわりに
クエリを組み立てたカスタムで定義するところを経験できたので、類似のクエリが必要な場合には流用できそうでよかったです!
Kotlin / MongoDB / DDDはまだまだなので、とりあえず実装して詰まって解決してなるほど!という数をこなしていきたいと思います!!
以上です。
本記事が何かの一助になれば幸いです。
Discussion