---
title: PostgreSQLのJSON型とVIEWを使って異なるRead ModelとWrite Modelを作る (Spring Boot版)
tags: ["PostgreSQL", "Java", "Spring Boot"]
categories: ["Middleware", "RDBMS", "PostgreSQL"]
date: 2021-05-23T04:08:17Z
updated: 2021-05-23T04:11:43Z
---
このツイートを見て、面白そうだったのでSpring Bootで実装してみた。
特にRead用のデータモデルが決まっていない状態で、まずはとにかくデータを受け付けて後からRead Modelを設計したいケースを考える。
まずはPostgreSQLのJSONB型を使って、雑に任意のデータを受け付けるように作る。
```sql
-- CREATE WRITE MODEL
CREATE TABLE IF NOT EXISTS data
(
id SERIAL,
data JSONB
);
```
Spring Bootで実装する。
まずは雑なWrite Modelとして`Data`クラスを用意する。ここではJava 16のRecordsを使用する。
```java
package com.example.demopostgresjson.writemodel;
import java.util.Map;
public record Data(Integer id, Map data) {
}
```
次にMapperクラス。JSON型でinsertするのでプレースホルダに`::JSON`をつける必要がある。
```java
package com.example.demopostgresjson.writemodel;
import java.io.UncheckedIOException;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class DataMapper {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public DataMapper(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Transactional
public Data insert(Map data) {
try {
final String json = this.objectMapper.writeValueAsString(data);
final Integer id = this.jdbcTemplate.queryForObject("INSERT INTO data(data) values(?::JSON) RETURNING id", Integer.class, json);
return new Data(id, data);
}
catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
}
}
}
```
最後にControllerクラス。
```java
package com.example.demopostgresjson.writemodel;
import java.util.Map;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DataController {
public final DataMapper dataMapper;
public DataController(DataMapper dataMapper) {
this.dataMapper = dataMapper;
}
@PostMapping(path = "data")
public Data postData(@RequestBody Map data) {
return this.dataMapper.insert(data);
}
}
```
このアプリケーションに次の不定な型のリクエストを送る。
```
curl -s -H "Content-Type: application/json" -w '\n' localhost:8080/data -d '{"role":"author", "name": "foo", "books":["foo", "bar"]}'
curl -s -H "Content-Type: application/json" -w '\n' localhost:8080/data -d '{"foo":"bar"}'
curl -s -H "Content-Type: application/json" -w '\n' localhost:8080/data -d '{"role":"author", "name": "bar", "books":["baz"]}'
```
次のレスポンスが返る。
```json
{"id":1,"data":{"role":"author","name":"foo","books":["foo","bar"]}}
{"id":2,"data":{"foo":"bar"}}
{"id":3,"data":{"role":"author","name":"bar","books":["baz"]}}
```
PostgreSQL上には次のようにデータが格納されている。
```
demo=> select * from data;
id | data
----+------------------------------------------------------------
1 | {"name": "foo", "role": "author", "books": ["foo", "bar"]}
2 | {"foo": "bar"}
3 | {"name": "bar", "role": "author", "books": ["baz"]}
(3 rows)
```
ここでRead Model(`Author`と`Book`)が固まり、次のViewを作成する。
```sql
-- CREATE READ MODEL
CREATE INDEX IF NOT EXISTS role ON data ((data ->> 'role'));
CREATE OR REPLACE VIEW author AS
SELECT id AS author_id, data ->> 'name' AS author_name
FROM data
WHERE data ->> 'role' = 'author';
CREATE OR REPLACE VIEW book AS
SELECT id AS author_id, jsonb_array_elements_text(data -> 'books') AS book_name
FROM data
WHERE data ->> 'role' = 'author'
```
Read Modelとして`Author`クラスと`Book`クラスを作成する。
```java
package com.example.demopostgresjson.readmodel;
import java.util.List;
public record Author(Integer id, String name, List books) {
}
```
```java
package com.example.demopostgresjson.readmodel;
public record Book(String name) {
}
```
次に`Author`に対するMapperを作成する。
```java
package com.example.demopostgresjson.readmodel;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Repository;
@Repository
public class AuthorMapper {
private final JdbcTemplate jdbcTemplate;
private final ResultSetExtractor> authorResultSetExtractor = rs -> {
Integer authorId = null;
String authorName = null;
final List books = new ArrayList<>();
while (rs.next()) {
if (authorId == null) {
authorId = rs.getInt("author_id");
}
if (authorName == null) {
authorName = rs.getString("author_name");
}
books.add(new Book(rs.getString("book_name")));
}
if (authorId == null) {
return Optional.empty();
}
return Optional.of(new Author(authorId, authorName, books));
};
public AuthorMapper(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Optional findById(Integer authorId) {
return this.jdbcTemplate.query("SELECT a.author_id, a.author_name, b.book_name FROM author AS a INNER JOIN book AS b ON a.author_id = b.author_id WHERE a.author_id = ?", this.authorResultSetExtractor, authorId);
}
}
```
最後にControllerクラスを作成。
```java
package com.example.demopostgresjson.readmodel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthorController {
private final AuthorMapper authorMapper;
public AuthorController(AuthorMapper authorMapper) {
this.authorMapper = authorMapper;
}
@GetMapping(path = "authors/{authorId}")
public ResponseEntity getAuthor(@PathVariable("authorId") Integer authorId) {
return ResponseEntity.of(this.authorMapper.findById(authorId));
}
}
```
このGET APIに対して次のリクエストを送る。
```
curl -s -w '\n' localhost:8080/authors/1
curl -s -w '\n' localhost:8080/authors/3
```
次のレスポンスが返る。
```
{"id":1,"name":"foo","books":[{"name":"foo"},{"name":"bar"}]}
{"id":3,"name":"bar","books":[{"name":"baz"}]}
```
PostgreSQL上には次のようにデータが格納されている。
```
demo=> select * from author;
author_id | author_name
-----------+-------------
1 | foo
3 | bar
(2 rows)
demo=> select * from book;
author_id | book_name
-----------+-----------
1 | foo
1 | bar
3 | baz
(3 rows)
```
面白い。
データモデルを定まらない場合や軽量なCQRSなんかに使えそう。
完全なソースコードは[こちら](https://github.com/making/demo-postgres-json)。